duxact
v2.4.2
Published
Application state management for React
Downloads
5
Maintainers
Readme
Duxact
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.
Installation
npm install --save duxact
Compatibility
| React Version | Duxact Compatibility | |--|--| | React@16 | ^2.0.0 | | React@15 | ^1.0.0 |
Usage
Defining the Store (centralized state, a single source of truth)
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' });
Providing the Store to the application
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>
);
Consuming the State
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);
Updating the application state by Dispatching Actions
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);
API
connect
connect
is used to consume the state and dispatch the actions to update the state.
Arguments
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 thedispatch
function 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,false
indiates that the state has been changed. So returningfalse
will 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
connectState
can be used instead of connect
when a component only needs to consume the state and does not need to dispatch any actions.
Arguments
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,false
indiates that the state has been changed. So returningfalse
will 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
connectDispatch
can be used instead of connect
when a component only needs to dispatch actions and does not need to consume the state.
Arguments
mapDispatchToProps
: A function which receives thedispatch
function 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);
Hooks
useSelector
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.
Arguments
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,false
indiates that the state has been changed. So returningfalse
will 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
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;
Deep comparison
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
UserDetails
has re-rendered thenUserDetails
will 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);
Using custom equality function
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>
);
};
Helpers
arrayToMapStateToProps
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
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
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);
})
}
});
License
MIT