Speedux
State management for React with Redux, made easier.
Contents
Installation
Install with npm
npm install --save speedux
Install with yarn
yarn add speedux
Demos
Todos App
Shopping Cart App
Quick Tutorial
Let's say you are building a simple counter that displays three buttons. One button increases the count when clicked, another button decreases the count and a third button would reset the count.
1. Wrap your app
Start by importing the Provider
component from Speedux then wrap your application with it.
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'speedux';
import Counter from './Counter';
const App = (
<Provider>
<Counter />
</Provider>
);
ReactDOM.render(App, document.getElementById('root'));
2. Create global state
Next, create the global state that you need to use in your app.
import { createGlobalState } from 'speedux';
createGlobalState({
name: 'counter',
state: {
count: 0,
},
actions: {
increaseCount: () => (currentState) => ({
count: currentState.count + 1,
}),
decreaseCount: () => (currentState) => ({
count: currentState.count - 1,
}),
resetCount: () => ({
count: 0,
}),
},
});
The createGlobalState
takes a configuration object which describes the global state and how it should be mutated using actions.
Each action (such as resetCount
) should return an object that describes the changes that should be made to the global state.
If the global state change depends on the previous global state, return a function instead of an object. For example, consider actions increaseCount
and decreaseCount
.
3. Use global state in the UI
Finally, you can read the global state and dispatch actions from any component.
import React from 'react';
import { useGlobalState, useActions } from 'speedux';
const Counter = () => {
const counterState = useGlobalState('counter');
const counterActions = useActions('counter');
return (
<div>
<h1>Count is: { counterState.count }</h1>
<button onClick={counterActions.increaseCount}>
Increase count
</button>
<button onClick={counterActions.decreaseCount}>
Decrease count
</button>
<button onClick={counterActions.resetCount}>
Reset count
</button>
</div>
);
};
export default Counter;
Asyncronous Actions
In a real world application, you might need to fetch data from a remote source and update the UI accordingly. For such cases, you can use an asyncronous action. To create an asyncronous action, simply use a generator function instead of a normal function.
Whenever your generator function yields an object, that object will be used to update the component state in the Redux store.
If your generator function yields a Promise object, the function execution will pause until that promise is resolved and the result will be passed to the generator function on the next call.
Here is an example:
createGlobalState({
name: 'dataFetcher',
state: {
loading: false,
data: null,
},
actions: {
* fetchData() {
yield { loading: true };
const response = yield fetch('/api/posts');
const data = yield response.json();
yield {
loading: false,
data,
};
},
},
});
Handling Errors
To handle errors in an asyncronous action, you can use .catch()
or you can check if the resolved response is an instance of Error
:
createGlobalState({
name: 'faultyDataFetcher',
state: {
loading: false,
data: null,
error: null,
},
actions: {
* fetchData() {
yield { loading: true };
const response = yield fetch('/api/posts').catch(e => {
console.log('Failed to fetch data');
return { failed: true, posts: [] };
});
if (response.failed === true) {
...
} else {
...
}
},
* fetchOtherData() {
yield { loading: true };
const response = yield fetch('/api/posts');
if (response instanceof Error) {
yield { error: response.message };
} else {
...
}
},
},
});
Listening to Actions
You can use the handlers
configuration option to listen to any action dispatched by the Redux store.
Simply, use the action type as the key and the handler function as the value. The handler function will always receive the action object as a single parameter and should return an object that specifies the state keys that need to be updated and their new values.
Here is an example:
createGlobalState({
name: 'routerSpy',
state: { currentPath: null },
handlers: {
'@@router/LOCATION_CHANGE': (action) => {
return {
currentPath: action.payload.location.pathname,
};
},
},
});
You can also listen to actions that were defined in a configuration object
.
For example, if we have a global state foo
:
createGlobalState({
name: 'foo',
actions: {
saySomething(message) { ... }
},
...
});
And another global state baz
that needs to listen to action saySomething
which is defined in foo
:
createGlobalState({
name: 'baz',
state: {
text: null,
},
handlers: {
'foo.saySomething': function(action) {
return {
text: `Foo said: ${action.payload.message}!`
};
},
},
...
});
Dispatching Actions
You can use the useDispatch
hook to create a dispatch function.
Here is an example:
import { useDispatch } from 'speedux';
const MyComponent = () => {
const dispatch = useDispatch();
function onClickButton() {
dispatch({
type: 'someAction',
payload: {
value: 'abc',
},
});
}
return (
<div>
<button onClick={onClickButton}>
Dispatch an action
</button>
</div>
);
};
You can also dispatch actions that were defined in a configuration object.
For example, let's say that we have a global state profile
that displays the availability of a user:
createGlobalState({
name: 'profile',
state: {
userStatus: 'online',
},
actions: {
setUserStatus(userStatus) {
return { userStatus };
},
},
...
});
And you need to dispatch setUserStatus
action from component Baz
:
import { useDispatch } from 'speedux';
const Baz = () => {
const dispatch = useDispatch();
function setStatus(status) {
dispatch('profile.setUserStatus', status);
}
return (
<div>
<button onClick={() => setStatus('online')}>
Appear Online
</button>
<button onClick={() => setStatus('offline')}>
Appear Offline
</button>
</div>
);
};
Updating the State
Both action and handler functions define how the state should be updated by returning an object. This object specifies the state keys that need to be updated and their new values. In the following example, changeFirstName
will only update firstName
in the state with value Bingo
while lastName
will remain the same.
createGlobalState({
name: 'foo',
state: {
firstName: 'baz',
lastName: 'boo',
},
actions: {
changeFirstName() {
return { firstName: 'Bingo' };
}
}
});
const MyComponent = () => {
const state = useGlobalState('foo');
const actions = useActions('foo');
console.log(state);
return (
<div>
<button onClick={actions.changeFirstName}>
Click me
</button>
</div>
);
};
Nested State Keys
To update deeply nested state keys, you can use dot notation as a string:
createGlobalState({
name: 'myComponent',
state: {
data: {
list: [
{ props: { name: 'feeb' } },
{ props: { name: 'foo' } },
{ props: { name: 'fiz' } },
],
},
},
actions: {
changeFooName(newName) {
return { 'data.list[1].props.name': newName };
},
},
});
Wildcard Character: *
If you would like to modify all items of an array or an object in the state, use a wildcard character:
createGlobalState({
name: 'foo',
state: {
list: [
{ name: 'feeb' },
{ name: 'foo' },
{ name: 'fiz' },
],
},
actions: {
changeAllNames(newName) {
return { 'list.*.name': newName };
},
},
});
Mapper Function
If you need to dynamically calculate the new value of the state key based on the old value, return a function instead of an object:
createGlobalState({
name: 'counter',
state: {
count: 0,
},
actions: {
increaseCount: () => (prevState) => ({
count: prevState.count + 1,
}),
},
});
Middlewares
To use a middleware, import useMiddleware
method and pass it the middleware function. You don't need to use applyMiddleware
from Redux, this method will be called internally by Speedux.
Here is an example using React Router DOM (v5.1.2) and Connected React Router (v6.6.1):
import { Provider, useReducer, useMiddleware } from 'speedux';
import { ConnectedRouter, connectRouter, routerMiddleware } from 'connected-react-router';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
useReducer('router', connectRouter(history));
useMiddleware(routerMiddleware(history));
ReactDOM.render((
<Provider>
<ConnectedRouter history={history}>
...
</ConnectedRouter>
</Provider>
), document.getElementById('root'));
API
createGlobalState(configuration)
The createGlobalState
function creates a global state in the Redux store using the specified configuration object.
Example:
import { createGlobalState } from 'speedux';
createGlobalState({
name: 'foo',
state: {
value: 'abc',
},
actions: {
setValue(newValue) {
return { value: newValue };
},
},
});
useGlobalState(name)
Parameter | Type | Description |
---|
name | String | Name of the global state to retrieve. |
Once you have created a global state using createGlobalState
, you can use useGlobalState
hook in any component to retrieve that global state.
Example:
import { useGlobalState } from 'speedux';
const MyComponent = () => {
const state = useGlobalState('foo');
return (...);
};
useActions(name)
Parameter | Type | Description |
---|
name | String | Name of the global state to retrieve its actions. |
Once you have created a global state using createGlobalState
and defined actions that mutate that global state, you can use useActions
hook in any component to retrieve those actions.
Example:
import { useActions } from 'speedux';
const MyComponent = () => {
const actions = useActions('foo');
return (
<button onClick={() => actions.setValue('Hello world!')}>
Click to set value
</button>
);
};
useDispatch()
You can use the useDispatch
hook to create a dispatch function.
Example:
import { useDispatch } from 'speedux';
const MyComponent = () => {
const dispatch = useDispatch();
return (
<button onClick={() => dispatch({ type: 'SOME_ACTION' })}>
Click to dispatch an action
</button>
);
};
useHandler(actionType, callback)
Parameter | Type | Description |
---|
actionType | String | Type of the action to listen to. |
callback | Function | Function to be called when that action is dispatched. |
You can use the useHandler
hook to listen to any action dispatched by the Redux store.
Example:
import { useHandler } from 'speedux';
const MyComponent = () => {
useHandler('@@redux/INIT', () => {
console.log('INIT action has been dispatched');
});
return (...);
};
useReducer(key, reducer)
Allows registering a reducer function that can listen to any action dispatched by the store and modify the global state accordingly.
Parameter | Type | Description |
---|
key | String | A unique identifier key for the reducer. |
reducer | Function | Reducer function to use. |
Example:
import { useReducer } from 'speedux';
import { connectRouter } from 'connected-react-router';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
const routerReducer = connectRouter(history);
useReducer('router', routerReducer);
useMiddleware(middleware)
Allows using middleware functions such as React Router middleware and others. You don't need to use applyMiddleware
from Redux before passing the middleware to this function.
Parameter | Type | Description |
---|
middleware | Function | Middleware function to use. |
Example:
import { useMiddleware } from 'speedux';
import { routerMiddleware } from 'connected-react-router';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
useMiddleware(routerMiddleware(history));
The Configuration Object
The configuration object may contain one or more of the following keys:
name (String) - required
The name
key is the only required key in the configuration object. It must be unique for each global state as it is used to identify the global state in the Redux store.
state (Object)
Represents the piece of global state (or initial state) in the Redux store. If not provided, an empty object will be used.
actions (Object)
A list of all the actions that may need to be dispatched from the UI to update the state. Provide the action name as the key and the function as the value.
The key or function name will be used to generate the action type. For example, a name calculator
with a defined action addNumbers
will dispatch an action of type @@calculator/ADD_NUMBERS
whenever addNumbers()
is called.
The function should return an object that specifies the state keys that need to be updated and their new values.
createGlobalState({
name: 'calculator',
state: {
result: 0,
},
actions: {
addNumbers(x, y) {
return { result: x + y };
}
}
});
To create an asyncronous action, simply use a generator function instead of a normal function.
Whenever your generator function yields an object, that object will be used to update the component state in the Redux store.
If your generator function yields a Promise object, the function execution will pause until that promise is resolved and the result will be passed to the generator function on the next call.
See Asyncronous Actions for examples.
handlers (Object)
A list of all the external actions that may affect the global state. Provide the action type as the key and the handler function as the value. You can listen to any action dispatched by the Redux store.
The handler function will always receive the action object as a single parameter and should return an object that specifies the state keys that need to be updated and their new values.
See Listening to Actions for examples.
License
MIT