dev-notes/JavaScript/React/Redux.md

230 lines
6.6 KiB
Markdown
Raw Normal View History

# [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 creates and return an action object.
```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);
2021-09-20 19:35:32 +02:00
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
2021-09-20 19:35:32 +02:00
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 = ...
2021-09-20 19:35:32 +02:00
// specifies which state is passed to the component (called on state change)
const mapStateToProps = (state, ownProps /* optional */) => {
2021-09-20 19:35:32 +02:00
// 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
2021-09-20 19:35:32 +02:00
function mapDispatchToProps(dispatch) {
return {
// wrap action creators
actionCreator: (args) => dispatch(actionCreator(args))
};
}
// or
2021-09-20 19:35:32 +02:00
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.
2021-09-20 19:35:32 +02:00
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
2021-09-20 19:35:32 +02:00
// 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;
}
}
}
```