Application state management for React inspired by the Redux & React-Redux.
React-Redux is one of the great ways of managing application state, it has served thousands of applications across the globe and has worked wonders in my projects as well. It all started as a personal experiment on thinking about, how can the boiler plate be reduced or almost removed? Can the switch cases in the reduers be removed? Can reducer definitions be made more simple or are they even needed? If the actions are responsible for state changes then why not the same actions be responsible to do the work which reducers do? etc. I started experimenting on the implementation of these by looking at the Redux & React-Redux code. Finally I could come up with a solution, duxact.
duxact has adopted the Redux principles and duxact is based on its main conecpt Action is the Reducer. And with that, there is no need to define the Reducers separately and then combine them into one, which completely removes the need of Switch Cases in the Reducers.
npm install --save duxact
| React Version | Duxact Compatibility |
|---|---|
| React@16 | ^2.0.0 |
| React@15 | ^1.0.0 |
Store holds & provides the centralised application state. Use createStore to create the store which is to be supplied to the state provider. createStore can be supplied with the initial state of the application.
In the example below the createStore method accepts the initial state and creates the store.
import { createStore, Provider } from 'duxact';
const store = createStore({ initial: 'state' });
Provider makes the store available for all the child components, by holding the store in the context. It's best to define the <Provider/> component at the top most level of the application hierarchy, so that the store is available for all of the application components responsible for managing the application state.
In the example below the <Provider/> is created at the top most level in the components hierarchy and supplied the store.
import { createStore, Provider } from 'duxact';
const store = createStore({ initial: 'state' });
const APP = () => (
<Provider store={store}>
...
</Provider>
);
The components can access the state by connecting to the store. This connection is done by subscribing to the state. To subscribe to the state and its changes, a combination of higher order component connect & state-to-props mapping (also called as selector) is used. Let's name this mapping function as mapStateToProps. The function mapStateToProps receives the complete application state object and should return only the required state (which the consuming/subscribing component is expected to receive) from the complete application state object. So the function mapStateToProps is nothing but a selector of state which selects only the needed data from state and passes it as props to the component. mapStateToProps is called every time there is a change in the application state. The function mapStateToProps receives the latest state.
duxact uses deep comparison to detect changes in the selected state and it triggers rerendering only if the selected state has changed.
The higher order components connect or connectState are used to subscribe for the state changes by using the mapStateToProps mapping/selector. connect or connectState accepts mapStateToProps as an argument.
In the example below the component DarkThemeView needs to know if the dark theme mode is ON/OFF. The mapStateToProps function gets the darkTheme value from the current application state and returns only the needed state i.e. darkTheme value in the form of props to the DarkThemeView component. The component DarkThemeView receives a prop named darkTheme holding the current value. connect accepts the mapStateToProps as an argument and returns a function which is expecting the component DarkThemeView as the argument.
import { connect } from 'duxact';
// Map the state as props to the component
// An object mapping the current value of darkTheme is returned
const mapStateToProps = (currentState) => ({
darkTheme: currentState.darkTheme
});
// This object return from mapStateToProps is mapped as props & supplied to the component
// darkTheme is received as a property
const DarkThemeLabel = ({ darkTheme }) => (
<label>
Dark theme is {darkTheme? 'ON' : 'OFF'}
</label>
);
// connect the state to component
const DarkThemeView = connect(mapStateToProps)(DarkThemeLabel);
Actions are responsible for changing the application state. Actions are nothing but pure functions, which can be called by the Components on user actions, like enable/disable dark theme by clicking on a toggle button.
The higher order component connect or connectDispatch is used along with the the actions-to-props mapping function to supply the actions as props to the component. Let's name this mapping function as mapDispatchToProps. mapDispatchToProps receives an argument dispatch. This dispatch function, when called, instructs the store to update the state. The dispatch function expects a function as its only argument. Let's call this function as the reducer. The reducer is nothing but the state updater. The reducer receives the current state and must return the updated state. So when the dispatch is called with the reducer as an argument then to get the updated state the store executes the reducer function, by supplying the current state to the reducer, then it stores the changed state in the store and publishes the updated state (complete state object) to all the components who have subscribed for the state changes.
Actions can be called with any arguments, which are needed to update the state. Like, updateUserDetails action can be called with user data. This user data then can be updated in the application state.
The higher order components connect or connectDispatch are used to supply the actions as props to the component by accepting the mapDispatchToProps mapping function as its argument.
In the example below, component ToggleButton expects a function named toggleTheme to be supplied as a property, which will be called to toggle the dark theme mode. mapDispatchToProps receives dispatch and returns an object holding the function toggleTheme which acts as the action. The action toggleTheme calls the dispatch function with a function as an argument which is called as reducer. The reducer receives the current state and returns the updated state. This returned updated state is merged in the application state by the store. The changed state is supplied to the subscriber components, like the DarkThemeLabel defined in the example above, in the Consuming the State section.
import { connect } from 'duxact';
// Map the actions as props to the component
const mapDispatchToProps = dispatch => ({
toggleTheme: (/* arguments as needed */) => {
// Reducer receives the current state and returns the updated state
const reducer = (currentState) => ({
darkTheme: !currentState.darkTheme
});
// The dispatch is called with a function which acts as the reducer
dispatch(reducer);
}
});
// Button receives the action toggleTheme
const ToggleButton = ({ toggleTheme }) => (
<button
onClick={() => {toggleTheme()}}
>
Toggle
</button>
);
// connect the actions with store
const ThemeToggler = connect(null, mapDispatchToProps)(ToggleButton);
connect is used to consume the state and dispatch the actions to update the state.
mapStateToProps: (optional) A mapping function or a selector function, which receives the application state and should return the state (filtered out of the application state) required by the component. The state returned by the mapping function or selector is passed as props to the component.mapDispatchToProps: (optional) A function which receives thedispatchfunction to dispatch the actions. This function should return an object of actions, actions responsible for updating the application state. These actions are nothing but functions which are passed as props to the component.areEqual: (optional) An equality function to compare the old vs new state. This function receives oldState and newState. The function is expected to return boolean result,falseindiates that the state has been changed. So returningfalsewill trigger the rerendering.duxact, by default, deep compares the old & new states. (refer section Using custom equality function for more details)
import { connect } from 'duxact';
// Map the state as props to the component
// An object mapping the current value of darkTheme is returned
const mapStateToProps = (currentState) => ({
darkTheme: currentState.darkTheme
});
// Map the actions as props to the component
const mapDispatchToProps = dispatch => ({
toggleTheme: () => {
// Reducer receives the current state and returns the updated state
const reducer = (currentState) => ({
darkTheme: !currentState.darkTheme
});
// The dispatch is called with a function which acts as the reducer
dispatch(reducer);
}
});
// Button receives the state `darkTheme` and the action `toggleTheme`
const ToggleButton = ({ darkTheme, toggleTheme }) => (
<button
onClick={() => {toggleTheme()}}
>
{darkTheme? 'Apply light theme' : 'Apply dark theme'}
</button>
);
// connect the actions with store
const ThemeToggler = connect(mapStateToProps, mapDispatchToProps)(ToggleButton);
connectState can be used instead of connect when a component only needs to consume the state and does not need to dispatch any actions.
mapStateToProps: A mapping function or a selector function, which receives the application state and should return the state (filtered out of the application state) required by the component. The state returned by the mapping function or selector is passed as props to the component.areEqual: (optional) An equality function to compare the old vs new state. This function receives oldState and newState. The function is expected to return boolean result,falseindiates that the state has been changed. So returningfalsewill trigger the rerendering.duxact, by default, deep compares the old & new states. (refer section Using custom equality function for more details)
import { connectState } from 'duxact';
...
const DarkThemeView = connectState(mapStateToProps)(DarkThemeLabel);
connectDispatch can be used instead of connect when a component only needs to dispatch actions and does not need to consume the state.
mapDispatchToProps: A function which receives thedispatchfunction to dispatch the actions. This function should return an object of actions, actions responsible for updating the application state. These actions are nothing but functions which are passed as props to the component.
import { connectDispatch } from 'duxact';
...
const ThemeToggler = connectDispatch(mapDispatchToProps)(ToggleButton);
useSelector hook is used to consume the state. A selector function is passed to the hook useSelector. This selector function receives the application state and should return only the required state, filtered out from the application state.
selector: A mapping function or a selector function, which receives the application state and should return the state (filtered out of the application state) required by the component. The state returned by the mapping function or selector is passed as props to the component.areEqual: (optional) An equality function to compare the old vs new state. This function receives oldState and newState. The function is expected to return boolean result,falseindiates that the state has been changed. So returningfalsewill trigger the rerendering.duxact, by default, deep compares the old & new states. (refer section Using custom equality function for more details)
import { useSelector } from 'duxact';
const DarkThemeLabel = ({ darkTheme }) => {
// A function is passed to `useSeletor`
// This function receives the application state and returns the `darkTheme` values from the state
const darkTheme = useSelector((state) => (state.darkTheme));
return (
<label>
Dark theme is {darkTheme? 'ON' : 'OFF'}
</label>
);
};
export default DarkThemeLabel;
useDispatch hook returns dispatch function, which can be called to dispatch actions to update the state.
import { useDispatch } from 'duxact';
// Button receives the action toggleTheme
const ToggleButton = ({ toggleTheme }) => {
// Get the dispatch function
const dispatch = useDispatch();
const toggleTheme: () => {
// Reducer receives the current state and returns the updated state
const reducer = (currentState) => ({
darkTheme: !currentState.darkTheme
});
// The dispatch is called with a function which acts as the reducer
dispatch(reducer);
};
return (
<button
onClick={() => {toggleTheme()}}
>
Toggle
</button>
);
};
export default ToggleButton;
duxact deep compares the old selected state and new selected state. It updates the consumer only if new state has changed with respect to the old state. This avoids unnecessary re-renders of the consumer components.
In below example, component UserDetails will receive fresh props, name & address, only if name and/or address of the loggedInUser object gets updated in store. Because the mapStateToProps (selector) returns only the name & address fields of loggedInUser. So even if other data like dateOfBirth, age etc of the loggedInUser are changed, the consumer component UserDetails do not receive freshly mapped name & address, to avoid re-rendering of UserDetails.
Please note, if any parent component in the hierarchy of the
UserDetailshas re-rendered thenUserDetailswill also re-render. Its a default behavior of react components. To avoid this use React.PureComponent or React.memo or shouldComponentUpdate.
import { connect } from 'duxact';
const mapStateToProps = (currentState) => ({
name: currentState.loggedInUser.name,
address: currentState.loggedInUser.address
});
const UserDetails = ({ name, address }) => (
<div>
<label>{name}</label>
<label>{address}</label>
</div>
);
// connect the state to component
const UserDetailsView = connect(mapStateToProps)(UserDetails);
If the mapStateToProps or state selector function returns an object, and if any value in that object changes, then it will trigger a rerender. In the example below, the selected state loggedInUser is an object holding the use details. So if address is changed in the loggedInUser object then it will rerender both UserName and UserAuthorizationList components.
import { connect } from 'duxact';
const mapStateToProps = (currentState) => ({
loggedInUser: currentState.loggedInUser
});
const UserNameLabel = ({ loggedInUser }) => (
<label>{loggedInUser.name}</label>
);
const renderUserAuthorization = (authorization) => (
<div>
<label>{authorization.name}</label>
<label>{authorization.linkedFunctionalityName}</label>
</div>
);
const UserAuthorizationListView = ({ loggedInUser }) => (
<div>{loggedInUser.authorizations.map(renderUserAuthorization)}</div>
);
const UserName = connect(mapStateToProps)(UserNameLabel);
const UserAuthorizationList = connect(mapStateToProps)(UserAuthorizationListView);
To avoid unnecessary renders, always deep-select only the requried state.
import { connect } from 'duxact';
const mapStateToPropsForNameComponent = (currentState) => ({
name: currentState.loggedInUser.name
});
const UserNameLabel = ({ name }) => (
<label>{name}</label>
);
const UserName = connect(mapStateToPropsForNameComponent)(UserNameLabel);
import { connect } from 'duxact';
const mapStateToPropsForAuthorizationListComponent = (currentState) => ({
autnorizations: currentState.loggedInUser.autnorizations
});
const renderUserAuthorization = (authorization) => (
<div>
<label>{authorization.name}</label>
<label>{authorization.linkedFunctionalityName}</label>
</div>
);
const UserAuthorizationListView = ({ autnorizations }) => (
<div>{authorizations.map(renderUserAuthorization)}</div>
);
const UserAuthorizationList = connect(mapStateToPropsForAuthorizationListComponent)(UserAuthorizationListView);
connect, connectState & useSelector accepts a comparison function. This function receives oldState and newState. The function is expected to return boolean result, false indiates that the state has been changed. So returning false will trigger the rerendering.
import { connect, connectState } from 'duxact';
const areEqual = (oldState, newState) => {
return oldState.loggedInUser.updatedTimeStamp === newState.loggedInUser.updatedTimeStamp;
};
const mapStateToProps = (currentState) => ({
name: currentState.loggedInUser.name,
address: currentState.loggedInUser.address
});
const UserNameLabel = ({ name }) => (
<label>{name}</label>
);
const UserAddressLabel = ({ address }) => (
<label>{address}</label>
);
const UserName = connect(mapStateToProps, null, areEqual)(UserNameLabel);
const UserAddress = connectState(mapStateToProps, areEqual)(UserAddressLabel);
import { useSelector } from 'duxact';
const areEqual = (oldState, newState) => {
return oldState.loggedInUser.updatedTimeStamp === newState.loggedInUser.updatedTimeStamp;
};
const stateSelector = (currentState) => ({
address: currentState.loggedInUser.address
});
const UserAddress = () => {
const { address } = useSelector(stateSelector, areEqual);
return (
<label>{address}</label>
);
};
arrayToMapStateToProps is a shorthand style for defining the state mapping. All the string values provided in an array to arrayToMapStateToProps are supplied to the component as props.
Best suitable when the state values are to be supplied to component with same names. i.e. in the example below the darkTheme from the state is supplied to the component as property with name darkTheme.
const mapStateToProps = arrayToMapStateToProps(['darkTheme']);
same as
const mapStateToProps = (currentState) => ({
darkTheme: currentState.darkTheme
});
injectDispatch injects the dispatch function as a first argument in the actions. This enables loose coupling of the actions with mapDispatchToProps. The actions can be defined in a separate file.
// toggle-theme-action.js
const toggleTheme = (dispatch, ...restArgs) => {
const reducer = (currentState) => ({
darkTheme: !currentState.darkTheme
});
dispatch(reducer);
};
export default toggleTheme;
// toggle-theme-button.js
import toggleTheme from './toggle-theme-action';
const mapDispatchToProps = injectDispatch({ toggleTheme });
export default connect(null, mapDispatchToProps)(ToggleButton);
same as
const mapDispatchToProps = dispatch => ({
toggleTheme: () => {
const reducer = (currentState) => ({
darkTheme: !currentState.darkTheme
});
dispatch(reducer);
}
});
export default connect(null, mapDispatchToProps)(ToggleButton);
Async calls can be devided into two parts. One start of the API call and then update the recieved data in the state. Before calling the API, loading state can be set in state. After receiving the response from the async API, update the state using the reducer defined inside the action. No middlewares needed to handle the async actions.
In the example below, the after getting the data from API /api/user, dispatch is called with the reducer which updated the state. So no middleware is required here.
const mapDispatchToProps = dispatch => ({
getUserDetails: (userId) => {
const setLoadingStateReducer = (currentState) => ({
loadingUserDetails: true
});
dispatch(setLoadingStateReducer);
fetch('/api/user',args)
.then((resp) => resp.json())
.then((data) => {
const reducer = (currentState) => ({
userDetails: data.userDetails,
loadingUserDetails: false,
});
dispatch(reducer);
})
.catch((error) => {
const errorReducer = (currentState) => ({
userDetailsFetchError: error
loadingUserDetails: false,
});
dispatch(errorReducer);
})
}
});
MIT
