mirror of
https://github.com/m-lamonaca/dev-notes.git
synced 2025-06-08 18:57:12 +00:00
Rename all file to kebab-case
This commit is contained in:
parent
ffc085d78a
commit
68ca4e4f86
117 changed files with 0 additions and 1260 deletions
103
javascript/react/react-router.md
Normal file
103
javascript/react/react-router.md
Normal file
|
@ -0,0 +1,103 @@
|
|||
# [React Router](https://reactrouter.com)
|
||||
|
||||
Popular routing library. Allows to specify a route through React components, declaring which component is to be loaded for a given URL.
|
||||
|
||||
Key Components:
|
||||
|
||||
- **Router**: wrap the app entry-point, usually `BrowserRouter`
|
||||
- **Route**: "Load this component for this URL"
|
||||
- **Link**: react-managed anchors that won't post back to the browser
|
||||
|
||||
## Routers
|
||||
|
||||
Router Types:
|
||||
|
||||
- *HashRouter*: `#route`, adds hashes to the URLs
|
||||
- *BrowserRouter*: `/route`, uses HTML5 history API to provide clean URLs
|
||||
- *MemoryRouter*: no URL
|
||||
|
||||
```js
|
||||
// index.js
|
||||
|
||||
//other imports ...
|
||||
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
|
||||
React.render(
|
||||
<Router>
|
||||
<App />
|
||||
</Router>,
|
||||
document.getElementById("DomID");
|
||||
)
|
||||
```
|
||||
|
||||
```js
|
||||
// Component.js
|
||||
import { Route, Route } from "react-router-dom";
|
||||
|
||||
<div>
|
||||
{/* match route pattern exactly, all sub-routes will be matched otherwise */}
|
||||
<Route path="/" exact element={<Component props={props} />} />
|
||||
<Route path="/route" element={<Component props={props} />} />
|
||||
...
|
||||
</div>
|
||||
|
||||
// only one child can match, similar to Route-case
|
||||
<Routes>
|
||||
<Route path="/" exact element={<Component props={props} />} />
|
||||
<Route path="/route" element={<Component props={props} />} />
|
||||
<Route component={PageNotFound} /> {/* matches all non-existent URLs */}
|
||||
</Route>
|
||||
```
|
||||
|
||||
### URL Parameters & Query String
|
||||
|
||||
```js
|
||||
// Given
|
||||
<Route path="/route/:placeholder" element={<Component props={props} />} />
|
||||
// URL: app.com/route/sub-route?param=value
|
||||
|
||||
function Component(props) {
|
||||
props.match.params.placeholder; // sub-route
|
||||
props.location.query; // { param: value }
|
||||
props.location.pathname; // /route/sub-route?param=value
|
||||
}
|
||||
```
|
||||
|
||||
### Redirecting
|
||||
|
||||
```js
|
||||
import { Redirect } from "react-router-dom";
|
||||
|
||||
// redirects to another URL, should'nt be rendered on component mount but after an action
|
||||
<Redirect to="/route" />
|
||||
<Redirect from="/old-route" to="/new-route" />
|
||||
{ condition && <Redirect to="/route" /> } // redirect if condition is true
|
||||
|
||||
// or redirect manipulating the history (always in props)
|
||||
props.history.push("/new-route");
|
||||
```
|
||||
|
||||
### Prompts
|
||||
|
||||
```js
|
||||
import { Prompt } from "react-router-dom";
|
||||
|
||||
// displays a prompt when the condition is true
|
||||
<Prompt when={condition} message="prompt message" />
|
||||
```
|
||||
|
||||
## Link
|
||||
|
||||
Clicks on a link created with React-Router will be captured by react an all the routing will happen client side.
|
||||
|
||||
```js
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
// TARGET: <Route path="/route/:itemId" />
|
||||
<Link to="/route/1">Text</Link>
|
||||
|
||||
// add styling attributes to the rendered element when it matches the current URL.
|
||||
<NavLink to="/route" exact activeClassName="class">Text</NavLink>
|
||||
<NavLink to="/route" activeStyle={ { cssProp: value } }>Text</NavLink>
|
||||
```
|
150
javascript/react/react-tests.md
Normal file
150
javascript/react/react-tests.md
Normal file
|
@ -0,0 +1,150 @@
|
|||
# Testing React
|
||||
|
||||
## [Jest](https://jestjs.io/)
|
||||
|
||||
### Jest Configuration
|
||||
|
||||
```js
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json', 'node'],
|
||||
setupFilesAfterEnv: ['@testing-library/jest-dom/extend-expect'], // add testing-library methods to expect()
|
||||
transform: { '^.+\\.tsx?$': 'ts-jest'} // use ts-jest fo ts files
|
||||
}
|
||||
```
|
||||
|
||||
### Jest Tests
|
||||
|
||||
[Expect docs](https://jestjs.io/docs/expect)
|
||||
|
||||
```js
|
||||
// .spec.js or .test.js
|
||||
it("test description", () => {
|
||||
// test body
|
||||
expect(expected).toEqual(actual);
|
||||
});
|
||||
|
||||
// group related tests
|
||||
describe("test group name", () => {
|
||||
it(/* ... */);
|
||||
it(/* ... */);
|
||||
});
|
||||
```
|
||||
|
||||
### Snapshots
|
||||
|
||||
In `Component.Snapshots.js`:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
import renderer from "react-test-renderer";
|
||||
|
||||
import Component from "./path/to/Component";
|
||||
// import mock data if necessary
|
||||
|
||||
it("test description", () => {
|
||||
// renders the DOM tree of the component
|
||||
const tree = renderer.create(<Component funcProp={jest.fn() /* mock function */} /* component props */ />);
|
||||
|
||||
// save a snapshot of the component at this point in time ( in __snapshots__ folder)
|
||||
// in future test it will be checked to avoid regressions
|
||||
// can be updated during jest --watch pressing "u"
|
||||
expect(tree).matchSnapshot();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [Enzyme](https://enzymejs.github.io/enzyme/)
|
||||
|
||||
### Enzyme Configuration
|
||||
|
||||
```js
|
||||
// testSetup.js
|
||||
import { configure } from "enzyme";
|
||||
import Adapter from "enzyme-adapter-react-<version>";
|
||||
|
||||
configure({ adapter: new Adapter() });
|
||||
```
|
||||
|
||||
### Enzyme Tests
|
||||
|
||||
In `Component.test.js`:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
// eventual wrapper components (react-router, react-redux's provider, ...) for mount render
|
||||
|
||||
// shallow renders single component w/o children, no DOM generated
|
||||
// mount renders component w/ it's children
|
||||
|
||||
import Component from "./path/to/Component";
|
||||
|
||||
// factory to setup shallow test easily
|
||||
function testHelper(args) {
|
||||
const defaultProps = { /* default value for props in each test */ };
|
||||
|
||||
const props = { ...defaultProps, ...args };
|
||||
return shallow(<Component {...props} />);
|
||||
}
|
||||
|
||||
// shallow rendering test
|
||||
it("test description", () => {
|
||||
const dom = testHelper(/* optional args */);
|
||||
// or
|
||||
const dom = shallow(<Component /* props */ />);
|
||||
|
||||
// check a property of expected component
|
||||
// selector can be from raw JSX (name of a component)
|
||||
expect(dom.find("selector").property).toBe(expected);
|
||||
});
|
||||
|
||||
// mount rendering test
|
||||
if("test description" () => {
|
||||
const dom = mount(
|
||||
<WrapperComponent>
|
||||
<Component /* props *//>
|
||||
</WrapperComponent>
|
||||
);
|
||||
|
||||
// selector has to be HTML selector since the component is rendered completely
|
||||
// possible to test child components
|
||||
expect(dom.find("selector").property).toBe(expected);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
|
||||
|
||||
Encourages to write test based on what the user sees. So components are always *mounted* and fully rendered.
|
||||
|
||||
### React Testing Library Tests
|
||||
|
||||
In `Components.test.js`:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
import { cleanup, render } from "@testing-library/react";
|
||||
|
||||
import Component from "./path/to/Component";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
// factory to setup test easily
|
||||
function testHelper(args) {
|
||||
const defaultProps = { /* default value for props in each test */ };
|
||||
|
||||
const props = { ...defaultProps, ...args };
|
||||
return render(<Component {...props} />);
|
||||
}
|
||||
|
||||
it("test description", () => {
|
||||
const { getByText } = testHelper();
|
||||
|
||||
// react testing library func
|
||||
getByText("text"); // check if test is present in the rendered component
|
||||
});
|
||||
```
|
261
javascript/react/react.md
Normal file
261
javascript/react/react.md
Normal file
|
@ -0,0 +1,261 @@
|
|||
# React
|
||||
|
||||
## Components
|
||||
|
||||
There are two types of react components:
|
||||
|
||||
- Function Components
|
||||
- Class Components
|
||||
|
||||
Both types can be stateful and have side effects or be purely presentational.
|
||||
|
||||
```jsx
|
||||
// functional component
|
||||
const Component = (props) => {
|
||||
return (
|
||||
<domElementOrComponent... />
|
||||
);
|
||||
}
|
||||
|
||||
// class component
|
||||
class Component extends React.Component {
|
||||
return (
|
||||
<domElementOrComponent... />
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
*NOTE*: a component name *must* start with an uppercase letter.
|
||||
|
||||
Every components has two inputs: *props* and *state*. The props input is explicit while the state is implicit.
|
||||
State is used to determine the changes and when to re-render.
|
||||
Within the component state can be changed while the props object represent fixed input values.
|
||||
|
||||
JSX syntax can represent HTML but gets converted to pure JavaScript before being sent to the browser:
|
||||
|
||||
```js
|
||||
// JSX
|
||||
const element = (
|
||||
<h1 className="greeting">Hello, world!</h1>
|
||||
);
|
||||
|
||||
// compiled JS shipped to browser
|
||||
const element = React.createElement(
|
||||
'h1', // HTML tag name
|
||||
{className: 'greeting'}, // attrs as JSON
|
||||
'Hello, world!' // tag content (can be nested component)
|
||||
);
|
||||
```
|
||||
|
||||
### App Entry-point
|
||||
|
||||
```js
|
||||
const container = document.getElementById('root')!;
|
||||
const root = createRoot(container);
|
||||
|
||||
const element = <h1s>Hello World</h1>
|
||||
root.render(element)
|
||||
```
|
||||
|
||||
### Dynamic Expressions
|
||||
|
||||
```js
|
||||
<tag>{expression}</tag> // expression is evaluated an it's result is displayed
|
||||
<tag onEvent={funcReference}>{expression}</tag>
|
||||
<tag onEvent={() => func(args)}>{expression}</tag>
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
```js
|
||||
<Component propName={value} /> // pass a value the component
|
||||
<Component propName={funcReference} /> // pass a function to the component
|
||||
|
||||
function Component(props) {
|
||||
// use props with {props.propName}
|
||||
}
|
||||
|
||||
class Component extends React.Component{
|
||||
// use props with {this.props.propName}
|
||||
render()
|
||||
}
|
||||
```
|
||||
|
||||
### Simple Function Component
|
||||
|
||||
```js
|
||||
// Button.js
|
||||
import { useState } from "react";
|
||||
|
||||
function Button() {
|
||||
const [count, setCount] = useState(0); // hook
|
||||
|
||||
const handleCLick = () => setCount(count + 1); // logic
|
||||
|
||||
// JSX
|
||||
return (
|
||||
<button onClick={handleCLick}>
|
||||
{count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default Button;
|
||||
```
|
||||
|
||||
### Simple Class Component
|
||||
|
||||
```js
|
||||
class Button extends React.Component {
|
||||
|
||||
state = {count: 0};
|
||||
//or
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {count: 0};
|
||||
}
|
||||
|
||||
componentDidMount() {} // called on successful component mount
|
||||
|
||||
handleClick = () => {
|
||||
this.setState({ count: this.state.count + 1 });
|
||||
}
|
||||
// or
|
||||
handleClick = () => {
|
||||
this.setState((state, props) => ({ count: state.count + props.increment }) );
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<button onClick={this.handleClick}>
|
||||
{this.state.count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Nesting Components
|
||||
|
||||
```js
|
||||
import { useState } from "react";
|
||||
|
||||
function Button(props) {
|
||||
return (
|
||||
<button onClick={props.onClickFunc}>
|
||||
+1
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function Display (props) {
|
||||
return (
|
||||
<div>{props.message}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
||||
// state must be declare in the outer component it can be passed to each children
|
||||
const [count, setCount] = useState(0);
|
||||
const incrementCounter = () => setCount(count + 1);
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<Button onClickFunc={incrementCounter}/>
|
||||
<Display message={count}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### User Input (Forms)
|
||||
|
||||
```js
|
||||
function Form() {
|
||||
const [userName, setUserName] = useState("");
|
||||
|
||||
handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
// ...
|
||||
}
|
||||
|
||||
return(
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={userName} // controlled component
|
||||
onChange={(event) => setUserName(event.target.value)} // needed to update UI on dom change
|
||||
required
|
||||
/>
|
||||
<button></button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Lists of Components
|
||||
|
||||
```js
|
||||
// ...
|
||||
<div>
|
||||
{array.map(item => <Component key={uniqueID}>)}
|
||||
</div>
|
||||
// ...
|
||||
```
|
||||
|
||||
**NOTE**: The `key` attribute of the component is needed to identify a particular item. It's most useful if the list has to be sorted.
|
||||
|
||||
## Hooks
|
||||
|
||||
### `useState`
|
||||
|
||||
Hook used to create a state object.
|
||||
|
||||
`useState()` results:
|
||||
|
||||
- state object (getter)
|
||||
- updater function (setter)
|
||||
|
||||
```js
|
||||
const [state, setState] = useState(default);
|
||||
```
|
||||
|
||||
### `useEffect`
|
||||
|
||||
Hook used to trigger an action on each render of the component or when one of the watched items changes.
|
||||
|
||||
```js
|
||||
|
||||
useEffect(() => {
|
||||
// "side effects" operations
|
||||
|
||||
return () => {/* clean up side effect */} // optional
|
||||
}, [/* list of watched items, empty triggers once */]);
|
||||
```
|
||||
|
||||
### Custom Hooks
|
||||
|
||||
```js
|
||||
// hook definitions
|
||||
const useCustomHook = () => {
|
||||
// eventual state definitions
|
||||
|
||||
// eventual function definitions
|
||||
|
||||
// ...
|
||||
|
||||
return { obj1, obj2, ... };
|
||||
}
|
||||
|
||||
const Component(){
|
||||
// retrieve elements from the hook
|
||||
const {
|
||||
obj1,
|
||||
obj2,
|
||||
...
|
||||
} = useCustomHook();
|
||||
}
|
||||
```
|
156
javascript/react/redux-tests.md
Normal file
156
javascript/react/redux-tests.md
Normal file
|
@ -0,0 +1,156 @@
|
|||
# Redux Testing
|
||||
|
||||
## Tests for Connected Components
|
||||
|
||||
Connected components are wrapped in a call to `connect`. Way of solving the problem:
|
||||
|
||||
- Wrap component with `<Provider>`. Added benefit: new store dedicated to tests.
|
||||
- Add named export for unconnected component.
|
||||
|
||||
In `Component.js`:
|
||||
|
||||
```js
|
||||
export function Component(props) { /* ... */ } // export unconnected component
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Component) // default export of connected component
|
||||
```
|
||||
|
||||
In `Component.test.js`:
|
||||
|
||||
```js
|
||||
import React from "react";
|
||||
// import enzyme or react testing library
|
||||
|
||||
// import mock data
|
||||
import { Component } from "path/to/Component"; // import unconnected component
|
||||
|
||||
// factory to setup test easily
|
||||
function testHelper(args) {
|
||||
const defaultProps = {
|
||||
/* default value for props in each test and required props */,
|
||||
history = {} // normally injected by react-router, could also import the router
|
||||
};
|
||||
|
||||
const props = { ...defaultProps, ...args };
|
||||
return mount(<Component {...props} />); // or render if using react testing library
|
||||
}
|
||||
|
||||
it("test description", () => {
|
||||
const dom = testHelper();
|
||||
|
||||
// simulate page iteration
|
||||
dom.find("selector").simulate("<event>");
|
||||
|
||||
// find changed component
|
||||
// test expected behaviour of component
|
||||
});
|
||||
```
|
||||
|
||||
## Tests for Action Creators
|
||||
|
||||
```js
|
||||
import * as actions from "path/to/actionCreators";
|
||||
// import eventual action types constants
|
||||
// import mock data
|
||||
|
||||
it("test description", () => {
|
||||
const data = /* mock data */
|
||||
const expectedAction = { type: TYPE, /* ... */ };
|
||||
|
||||
const actualAction = actions.actionCreator(data);
|
||||
|
||||
expect(actualAction).toEqual(expectedAction);
|
||||
});
|
||||
```
|
||||
|
||||
## Tests for Reducers
|
||||
|
||||
```js
|
||||
import reducer from "path/to/reducer";
|
||||
import * as actions from "path/to/actionCreators";
|
||||
|
||||
it("test description", () => {
|
||||
const initialState = /* state before the action */;
|
||||
const finalState = /* expected state after the action */
|
||||
const data = /* data passed to the action creator */;
|
||||
|
||||
const action = actions.actionCreator(data);
|
||||
const newState = reducer(initialState, action);
|
||||
|
||||
expect(newState).toEqual(finalState);
|
||||
});
|
||||
```
|
||||
|
||||
## Tests for the Store
|
||||
|
||||
```js
|
||||
import { createStore } from "redux";
|
||||
|
||||
import rootReducer from "path/to/rootReducer";
|
||||
import initialState from "path/to/initialState";
|
||||
import * as actions from "path/to/actionCreators";
|
||||
|
||||
it("test description", () => {
|
||||
const store = createStore(storeReducer, initialState);
|
||||
|
||||
const expectedState = /* state after the update */
|
||||
|
||||
const data = /* action creator input */;
|
||||
const action = actions.actionCreator(data);
|
||||
store.dispatch(action);
|
||||
|
||||
const state = store.getState();
|
||||
expect(state).toEqual(expectedState);
|
||||
});
|
||||
```
|
||||
|
||||
## Tests for Thunks
|
||||
|
||||
Thunk testing requires the mocking of:
|
||||
|
||||
- store (using `redux-mock-store`)
|
||||
- HTTP calls (using `fetch-mock`)
|
||||
|
||||
```js
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
import configureMockStore from "redux-mock-store";
|
||||
|
||||
// needed for testing async thunks
|
||||
const middleware = [thunk]; // mock middlewares
|
||||
const mockStore = configureMockStore(middleware); // mock the store
|
||||
|
||||
import * as actions from "path/to/actionCreators";
|
||||
// import eventual action types constants
|
||||
// import mock data
|
||||
|
||||
describe("Async Actions", () => {
|
||||
afterEach(() => {
|
||||
fetchMock.restore(); // init fetch mock for each test
|
||||
});
|
||||
|
||||
it("test description", () => {
|
||||
// mimic API call
|
||||
fetchMock.mock(
|
||||
"*", // capture any fetch call
|
||||
{
|
||||
body: /* body contents */,
|
||||
headers: { "content-type": "application/json" }
|
||||
}
|
||||
);
|
||||
|
||||
// expected action fired from the thunk
|
||||
const expectedActions = [
|
||||
{ type: TYPE, /* ... */ },
|
||||
{ type: TYPE, /* ... */ }
|
||||
];
|
||||
|
||||
const store = mockStore({ data: value, ... }); // init mock store
|
||||
|
||||
return store.dispatch(actions.actionCreator()) // act
|
||||
.then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions); // assert
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
465
javascript/react/redux.md
Normal file
465
javascript/react/redux.md
Normal file
|
@ -0,0 +1,465 @@
|
|||
# [Redux](https://redux.js.org/)
|
||||
|
||||
Redux is a pattern and library for managing and updating application state, using events called *actions*. It serves as a centralized store for state that needs to be used across the entire application, with rules ensuring that the state can only be updated in a predictable fashion.
|
||||
|
||||
## Actions, Store, Immutability & Reducers
|
||||
|
||||
### Actions & Action Creators
|
||||
|
||||
An **Action** is a plain JavaScript object that has a `type` field. An action object can have other fields with additional information about what happened.
|
||||
By convention, that information is stored in a field called `payload`.
|
||||
|
||||
**Action Creators** are functions that create and return action objects.
|
||||
|
||||
```js
|
||||
function actionCreator(data)
|
||||
{
|
||||
return { type: ACTION_TYPE, payload: data }; // action obj
|
||||
}
|
||||
```
|
||||
|
||||
### Store
|
||||
|
||||
The current Redux application state lives in an object called the **store**.
|
||||
The store is created by passing in a reducer, and has a method called `getState` that returns the current state value.
|
||||
|
||||
The Redux store has a method called `dispatch`. The only way to update the state is to call `store.dispatch()` and pass in an action object.
|
||||
The store will run its reducer function and save the new state value inside.
|
||||
|
||||
**Selectors** are functions that know how to extract specific pieces of information from a store state value.
|
||||
|
||||
In `initialState.js`;
|
||||
|
||||
```js
|
||||
export default {
|
||||
// initial state here
|
||||
}
|
||||
```
|
||||
|
||||
In `configStore.js`:
|
||||
|
||||
```js
|
||||
// configStore.js
|
||||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
|
||||
export function configStore(initialState) {
|
||||
const composeEnhancers =
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // support for redux devtools
|
||||
|
||||
return createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
composeEnhancers(applyMiddleware(middleware, ...))
|
||||
);
|
||||
}
|
||||
|
||||
// available functions & methods
|
||||
replaceReducer(newReducer); // replace an existing reducer, useful for Hot Reload
|
||||
store.dispatch(action); // trigger a state change based on an action
|
||||
store.subscribe(listener);
|
||||
store.getState(); // retrieve current state
|
||||
```
|
||||
|
||||
### Reducers
|
||||
|
||||
**Reducers** are functions that receives the current state and an action, decide how to update the state if necessary, and return the new state.
|
||||
|
||||
Reducers must **always** follow some specific rules:
|
||||
|
||||
- They should only calculate the new state value based on the `state` and `action` arguments
|
||||
- They are not allowed to modify the existing `state`.
|
||||
Instead, they must make *immutable updates*, by copying the existing `state` and making changes to the copied values.
|
||||
- They must not do any asynchronous logic, calculate random values, or cause other "side effects"
|
||||
|
||||
```js
|
||||
import initialState from "path/to/initialState";
|
||||
|
||||
function reducer(state = initialState, action) {
|
||||
switch(action.type){
|
||||
case "ACTION_TYPE":
|
||||
return { ...state, prop: value }; // return modified copy of state (using spread operator)
|
||||
break;
|
||||
|
||||
default:
|
||||
return state; // return unchanged state (NEEDED)
|
||||
}
|
||||
}
|
||||
|
||||
// combining reducers
|
||||
import { combineReducers } from "redux";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
entity: entityReducer.
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
**NOTE**: multiple reducers can be triggered by the same action since each one operates on a different portion of the state.
|
||||
|
||||
## [React-Redux](https://react-redux.js.org/)
|
||||
|
||||
### Container vs Presentational Components
|
||||
|
||||
Container Components:
|
||||
|
||||
- Focus on how thing work
|
||||
- Aware of Redux
|
||||
- Subscribe to Redux State
|
||||
- Dispatch Redux actions
|
||||
|
||||
Presentational Components:
|
||||
|
||||
- Focus on how things look
|
||||
- Unaware of Redux
|
||||
- Read data from props
|
||||
- Invoke callbacks on props
|
||||
|
||||
### Provider Component & Connect
|
||||
|
||||
Used at the root component and wraps all the application.
|
||||
|
||||
```js
|
||||
// index.js
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { configStore } from 'path/to/configStore';
|
||||
import initialState from "path/to/initialState";
|
||||
import App from './App';
|
||||
|
||||
const store = configStore(initialState);
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>,
|
||||
rootElement
|
||||
);
|
||||
```
|
||||
|
||||
```js
|
||||
// Component.js
|
||||
import { connect } from 'react-redux';
|
||||
import { increment, decrement, reset } from './actionCreators';
|
||||
|
||||
// const Component = ...
|
||||
|
||||
// specifies which state is passed to the component (called on state change)
|
||||
const mapStateToProps = (state, ownProps /* optional */) => {
|
||||
// structure of the props passed to the component
|
||||
return { propName: state.property };
|
||||
};
|
||||
|
||||
// specifies the action passed to a component (the key is the name that the prop will have )
|
||||
const mapDispatchToProps = { actionCreator: actionCreator };
|
||||
// or
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
// wrap action creators
|
||||
actionCreator: (args) => dispatch(actionCreator(args))
|
||||
};
|
||||
}
|
||||
// or
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
actionCreator: bindActionCreators(actionCreator, dispatch),
|
||||
actions: bindActionCreators(allActionCreators, dispatch)
|
||||
};
|
||||
}
|
||||
|
||||
// both args are optional
|
||||
// if mapDispatch is missing the dispatch function is added to the props
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Component);
|
||||
```
|
||||
|
||||
## Async Operations with [Redux-Thunk](https://github.com/reduxjs/redux-thunk)
|
||||
|
||||
**Note**: Redux middleware runs *after* and action and *before* it's reducer.
|
||||
|
||||
Redux-Thunk allows to return functions instead of objects from action creators.
|
||||
A "thunk" is a function that wraps an expression to delay it's evaluation.
|
||||
|
||||
In `configStore.js`:
|
||||
|
||||
```js
|
||||
import { createStore, applyMiddleware, compose } from "redux";
|
||||
import thunk from "redux-thunk";
|
||||
|
||||
function configStore(initialState) {
|
||||
const composeEnhancers =
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; // support for redux devtools
|
||||
|
||||
return createStore(
|
||||
rootReducer,
|
||||
initialState,
|
||||
composeEnhancers(applyMiddleware(thunk, ...)) // add thunks middleware
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```js
|
||||
// usually action on async func success
|
||||
function actionCreator(arg) {
|
||||
return { type: TYPE, data: arg };
|
||||
}
|
||||
|
||||
export function thunk() {
|
||||
return function (dispatch) { // redux-thunk injects dispatch as arg
|
||||
return asyncFunction().then((data) => { // async function returns a promise
|
||||
dispatch(actionCreator(data));
|
||||
})
|
||||
.catch((error) => {
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// or using async/await
|
||||
export async function thunk() {
|
||||
return function (dispatch) { // redux-thunk injects dispatch as arg
|
||||
try {
|
||||
let data = await asyncFunction();
|
||||
return dispatch(actionCreator(data));
|
||||
} catch(error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## [Redux-Toolkit](https://redux-toolkit.js.org/)
|
||||
|
||||
The Redux Toolkit package is intended to be the standard way to write Redux logic. It was originally created to help address three common concerns about Redux.
|
||||
|
||||
Redux Toolkit also includes a powerful data fetching and caching capability dubbed "RTK Query". It's included in the package as a separate set of entry points. It's optional, but can eliminate the need to hand-write data fetching logic yourself.
|
||||
|
||||
These tools should be beneficial to all Redux users. Whether you're a brand new Redux user setting up your first project, or an experienced user who wants to simplify an existing application, Redux Toolkit can help you make your Redux code better.
|
||||
Installation
|
||||
Using Create React App
|
||||
|
||||
The recommended way to start new apps with React and Redux is by using the official Redux+JS template or Redux+TS template for Create React App, which takes advantage of Redux Toolkit and React Redux's integration with React components.
|
||||
|
||||
```sh
|
||||
# Redux + Plain JS template
|
||||
npx create-react-app my-app --template redux
|
||||
|
||||
# Redux + TypeScript template
|
||||
npx create-react-app my-app --template redux-typescript
|
||||
```
|
||||
|
||||
Redux Toolkit includes these APIs:
|
||||
|
||||
- [`configureStore()`][cfg_store]: wraps `createStore` to provide simplified configuration options and good defaults.
|
||||
It can automatically combines slice reducers, adds whatever Redux middleware supplied, includes redux-thunk by default, and enables use of the Redux DevTools Extension.
|
||||
|
||||
- [`createReducer()`][new_reducer]: that lets you supply a lookup table of action types to case reducer functions, rather than writing switch statements.
|
||||
In addition, it automatically uses the `immer` library to let you write simpler immutable updates with normal mutative code, like `state.todos[3].completed = true`.
|
||||
|
||||
- [`createAction()`][new_action]: generates an action creator function for the given action type string.
|
||||
The function itself has `toString()` defined, so that it can be used in place of the type constant.
|
||||
- [`createSlice()`][new_slice]: accepts an object of reducer functions, a slice name, and an initial state value, and automatically generates a slice reducer with corresponding action creators and action types.
|
||||
- [`createAsyncThunk`][new_async_thunk]: accepts an action type string and a function that returns a promise, and generates a thunk that dispatches pending/fulfilled/rejected action types based on that promise
|
||||
- [`createEntityAdapter`][entity_adapt]: generates a set of reusable reducers and selectors to manage normalized data in the store
|
||||
- The `createSelector` utility from the Reselect library, re-exported for ease of use.
|
||||
|
||||
[cfg_store]: https://redux-toolkit.js.org/api/configureStore
|
||||
[new_reducer]: https://redux-toolkit.js.org/api/createReducer
|
||||
[new_action]: https://redux-toolkit.js.org/api/createAction
|
||||
[new_slice]: https://redux-toolkit.js.org/api/createSlice
|
||||
[new_async_thunk]: https://redux-toolkit.js.org/api/createAsyncThunk
|
||||
[entity_adapt]: https://redux-toolkit.js.org/api/createEntityAdapter
|
||||
|
||||
### [`configureStore`](https://redux-toolkit.js.org/api/configureStore)
|
||||
|
||||
Included Default Middleware:
|
||||
|
||||
- Immutability check middleware: deeply compares state values for mutations. It can detect mutations in reducers during a dispatch, and also mutations that occur between dispatches.
|
||||
When a mutation is detected, it will throw an error and indicate the key path for where the mutated value was detected in the state tree. (Forked from `redux-immutable-state-invariant`.)
|
||||
|
||||
- Serializability check middleware: a custom middleware created specifically for use in Redux Toolkit
|
||||
Similar in concept to `immutable-state-invariant`, but deeply checks the state tree and the actions for non-serializable values such as functions, Promises, Symbols, and other non-plain-JS-data values
|
||||
When a non-serializable value is detected, a console error will be printed with the key path for where the non-serializable value was detected.
|
||||
|
||||
- In addition to these development tool middleware, it also adds `redux-thunk` by default, since thunks are the basic recommended side effects middleware for Redux.
|
||||
|
||||
Currently, the return value of `getDefaultMiddleware()` is:
|
||||
|
||||
```js
|
||||
// development
|
||||
const middleware = [thunk, immutableStateInvariant, serializableStateInvariant]
|
||||
|
||||
// production
|
||||
const middleware = [thunk]
|
||||
```
|
||||
|
||||
```js
|
||||
|
||||
import { combineReducers } from 'redux'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import monitorReducersEnhancer from './enhancers/monitorReducers'
|
||||
import loggerMiddleware from './middleware/logger'
|
||||
import usersReducer from './usersReducer'
|
||||
import postsReducer from './postsReducer'
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
users: usersReducer,
|
||||
posts: postsReducer,
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
// reducers combined automatically
|
||||
reducer: rootReducer,
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(loggerMiddleware),
|
||||
enhancers: [monitorReducersEnhancer]
|
||||
})
|
||||
|
||||
export default store
|
||||
```
|
||||
|
||||
### [`createAction`](https://redux-toolkit.js.org/api/createAction)
|
||||
|
||||
```js
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
const increment = createAction<number | undefined>('counter/increment');
|
||||
|
||||
const action = increment(); // { type: 'counter/increment' }
|
||||
const action = increment(3); // { type: 'counter/increment', payload: 3 }
|
||||
|
||||
increment.toString(); // 'counter/increment'
|
||||
```
|
||||
|
||||
### [`createReducer`](https://redux-toolkit.js.org/api/createReducer)
|
||||
|
||||
```js
|
||||
import { createAction, createReducer } from '@reduxjs/toolkit'
|
||||
|
||||
interface CounterState {
|
||||
value: number
|
||||
}
|
||||
|
||||
const increment = createAction('counter/increment')
|
||||
const decrement = createAction('counter/decrement')
|
||||
const incrementByAmount = createAction<number>('counter/incrementByAmount')
|
||||
|
||||
const initialState = { value: 0 } as CounterState
|
||||
|
||||
const counterReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(increment, (state, action) => {
|
||||
state.value++
|
||||
})
|
||||
.addCase(decrement, (state, action) => {
|
||||
state.value--
|
||||
})
|
||||
.addCase(incrementByAmount, (state, action) => {
|
||||
state.value += action.payload
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### [`createSlice`](https://redux-toolkit.js.org/api/createSlice)
|
||||
|
||||
A function that accepts an initial state, an object of reducer functions, and a "slice name", and automatically generates action creators and action types that correspond to the reducers and state.
|
||||
|
||||
Internally, it uses `createAction` and `createReducer`, so it's possible to use Immer to write "mutating" immutable updates.
|
||||
|
||||
**Note**: action types will have the `<slice-name>/<reducer-name>` shape.
|
||||
|
||||
```js
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
interface CounterState {
|
||||
value: number
|
||||
}
|
||||
|
||||
const initialState = { value: 0 } as CounterState
|
||||
|
||||
const counterSlice = createSlice({
|
||||
name: 'counter',
|
||||
initialState,
|
||||
reducers: {
|
||||
increment(state) {
|
||||
state.value++
|
||||
},
|
||||
decrement(state) {
|
||||
state.value--
|
||||
},
|
||||
incrementByAmount(state, action: PayloadAction<number>) {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions
|
||||
export default counterSlice.reducer
|
||||
```
|
||||
|
||||
### [`createAsyncThunk`](https://redux-toolkit.js.org/api/createAsyncThunk)
|
||||
|
||||
The function `createAsyncThunk` returns a standard Redux thunk action creator.
|
||||
The thunk action creator function will have plain action creators for the pending, fulfilled, and rejected cases attached as nested fields.
|
||||
|
||||
The `payloadCreator` function will be called with two arguments:
|
||||
|
||||
- `arg`: a single value, containing the first parameter that was passed to the thunk action creator when it was dispatched.
|
||||
- `thunkAPI`: an object containing all of the parameters that are normally passed to a Redux thunk function, as well as additional options:
|
||||
- `dispatch`: the Redux store dispatch method
|
||||
- `getState`: the Redux store getState method
|
||||
- `extra`: the "extra argument" given to the thunk middleware on setup, if available
|
||||
- `requestId`: a unique string ID value that was automatically generated to identify this request sequence
|
||||
- `signal`: an `AbortController.signal` object that may be used to see if another part of the app logic has marked this request as needing cancellation.
|
||||
- [...]
|
||||
|
||||
The logic in the `payloadCreator` function may use any of these values as needed to calculate the result.
|
||||
|
||||
```js
|
||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
const payloadCreator = async (arg, ThunkAPI): Promise<T> => { /* ... */ };
|
||||
const thunk = createAsyncThunk("<action-type>", payloadCreator);
|
||||
|
||||
thunk.pending; // action creator that dispatches an '<action-type>/pending'
|
||||
thunk.fulfilled; // action creator that dispatches an '<action-type>/fulfilled'
|
||||
thunk.rejected; // action creator that dispatches an '<action-type>/rejected'
|
||||
|
||||
const slice = createSlice({
|
||||
name: '<action-name>',
|
||||
initialState,
|
||||
reducers: { /* standard reducer logic, with auto-generated action types per reducer */ },
|
||||
extraReducers: (builder) => {
|
||||
// Add reducers for additional action types here, and handle loading state as needed
|
||||
builder.addCase(thunk.fulfilled, (state, action) => { /* body of the reducer */ })
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## RTK Query
|
||||
|
||||
RTK Query is provided as an optional addon within the `@reduxjs/toolkit` package.
|
||||
It is purpose-built to solve the use case of data fetching and caching, supplying a compact, but powerful toolset to define an API interface layer got the app.
|
||||
It is intended to simplify common cases for loading data in a web application, eliminating the need to hand-write data fetching & caching logic yourself.
|
||||
|
||||
RTK Query is included within the installation of the core Redux Toolkit package. It is available via either of the two entry points below:
|
||||
|
||||
```cs
|
||||
import { createApi } from '@reduxjs/toolkit/query'
|
||||
|
||||
/* React-specific entry point that automatically generates hooks corresponding to the defined endpoints */
|
||||
import { createApi } from '@reduxjs/toolkit/query/react'
|
||||
```
|
||||
|
||||
RTK Query includes these APIs:
|
||||
|
||||
- [`createApi()`][new_api]: The core of RTK Query's functionality. It allows to define a set of endpoints describe how to retrieve data from a series of endpoints,
|
||||
including configuration of how to fetch and transform that data.
|
||||
- [`fetchBaseQuery()`][fetch_query]: A small wrapper around fetch that aims to simplify requests. Intended as the recommended baseQuery to be used in createApi for the majority of users.
|
||||
- [`<ApiProvider />`][api_provider]: Can be used as a Provider if you do not already have a Redux store.
|
||||
- [`setupListeners()`][setup_listener]: A utility used to enable refetchOnMount and refetchOnReconnect behaviors.
|
||||
|
||||
[new_api]: https://redux-toolkit.js.org/rtk-query/api/createApi
|
||||
[fetch_query]: https://redux-toolkit.js.org/rtk-query/api/fetchBaseQuery
|
||||
[api_provider]: https://redux-toolkit.js.org/rtk-query/api/ApiProvider
|
||||
[setup_listener]: https://redux-toolkit.js.org/rtk-query/api/setupListeners
|
Loading…
Add table
Add a link
Reference in a new issue