Speedux
An opinionated library for managing state in React apps, based on Redux but much easier.
Contents
Installation
Install with npm
npm install --save speedux
Install with yarn
yarn add speedux
Demos
Todos App
Shopping Cart App
Quick Tutorial
Using Speedux is pretty easy and straight-forward. First step is to wrap your application in a Provider
component and the second step is to use the connect
function to connect your components to the store. Normal Redux stuff but with less code.
You can also use createHooks
if you prefer to use React hooks.
To understand how it works, let's take an example of a very simple counter app that displays three buttons. One button increases the count on click, another button decreases the count and a third button would reset the count.
1. Wrap your app
Start with the application entry file, it's usually the src/index.js file (assuming create-react-app). You would only need to import the Provider
component from Speedux and 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'));
That's pretty much all you need to do here.
2. Connect your component
We want the Counter
component state to contain a count
property with an initial value of zero. To update the count
property, we will use three actions: increaseCount
, decreaseCount
and resetCount
.
Connect using connect()
Import the connect
function from Speedux and pass it two parameters, the first parameter is your component definition and the second parameter is a configuration object that defines the initial state for your connected component and all the logic required to update it.
import React from 'react';
import { connect } from 'speedux';
const Counter = ({ state, actions }) => (
<div>
<h1>Count is: { state.count }</h1>
<button onClick={actions.increaseCount}>
Increase count
</button>
<button onClick={actions.decreaseCount}>
Decrease count
</button>
<button onClick={actions.resetCount}>
Reset count
</button>
</div>
);
export default connect(Counter, {
name: 'counter',
state: { count: 0 },
actions: {
increaseCount: () => (currentState) => ({
count: currentState.count + 1,
}),
decreaseCount: () => (currentState) => ({
count: currentState.count - 1,
}),
resetCount: () => ({
count: 0,
}),
},
});
Connect using hooks
Import the createHooks
function from Speedux and pass it a string as a single parameter. This function returns a set of React hooks that you can use to read and manipulate the global state of the component.
import React from 'react';
import { createHooks } from 'speedux';
const { useState, useActions } = createHooks('counter');
const Counter = () => {
const state = useState({ count: 0 });
const actions = useActions({
increaseCount: () => (currentState) => ({
count: currentState.count + 1,
}),
decreaseCount: () => (currentState) => ({
count: currentState.count - 1,
}),
resetCount: () => ({
count: 0,
}),
});
return (
<div>
<h1>Count is: { state.count }</h1>
<button onClick={actions.increaseCount}>
Increase count
</button>
<button onClick={actions.decreaseCount}>
Decrease count
</button>
<button onClick={actions.resetCount}>
Reset count
</button>
</div>
);
};
export default Counter;
That's it! You have a fully working counter component that is connected to the Redux store.
The connect
function automatically injected the state
and actions
props into the component props.
This was a very simple example to get you started. Next, you can learn more about the configuration object or keep reading to learn how to create asyncronous actions and listen to actions dispatched by other components.
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.
You can also return an array of Promises if you want to execute multiple requests at the same time.
Here is an example:
const config = {
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
:
const config = {
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 {
...
}
},
},
};
You can also use the handy function this.isError(response)
instead of response instanceof Error
.
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:
const config = {
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
of another connected component.
For example, if we have a connected component Foo
:
export default connect(Foo, {
name: 'foo',
actions: {
saySomething(message) { ... }
},
...
});
And another connected component Baz
that needs to listen to action saySomething
which would be dispatched by component Foo
:
export default connect(Baz, {
name: 'baz',
state: {
text: null,
},
handlers: {
'foo.saySomething': function(action) {
return {
text: `Foo said: ${action.payload.message}!`
};
},
},
...
});
Dispatching Actions
The connect
function automatically injects a dispatch
function into the component props. You can use the dispatch
function to dispatch any action and specify its payload as well.
Here is an example:
import React from 'react';
import { connect } from 'speedux';
const MyComponent = ({ dispatch }) => {
function onClickButton() {
dispatch({
type: 'someAction',
payload: {
value: 'abc',
},
});
}
return (
<div>
<button onClick={onClickButton}>
Dispatch an action
</button>
</div>
);
};
export default connect(MyComponent, {...});
You can also dispatch actions that were defined in a configuration object of another connected component.
For example, let's say that we have a component Profile
that displays the availability of a user:
export default connect(Profile, {
name: 'userProfile',
state: {
userStatus: 'online',
},
actions: {
setUserStatus(userStatus) {
return { userStatus };
},
},
...
})
And another component Baz
that needs to trigger the setUserStatus
action which is defined in the configuration object of component Profile
:
const Baz = ({ dispatch }) => {
function setStatus(status) {
dispatch('userProfile.setUserStatus', status);
}
return (
<div>
<button onClick={() => setStatus('online')}>
Appear Online
</button>
<button onClick={() => setStatus('offline')}>
Appear Offline
</button>
</div>
);
};
export default connect(Baz, {...})
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, changeFoo
will only update foo
in the state with value Bingo
while fiz
will remain the same.
const MyComponent = ({ state, actions }) => {
console.log(state);
return (
<div>
<button onClick={actions.changeFoo}>
Click me
</button>
</div>
);
};
export default connect(MyComponent, {
name: 'myComponent',
state: {
foo: 'baz',
fiz: 'boo',
},
actions: {
changeFoo() {
return { foo: 'Bingo' };
}
}
});
Nested State Keys
To update deeply nested state keys, you can use dot notation as a string:
export default connect(MyComponent, {
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:
export default createModule('foo', {
state: {
list: [
{ name: 'feeb' },
{ name: 'foo' },
{ name: 'fiz' },
],
},
actions: {
changeAllNames(newName) {
return { 'list.*.name': newName };
},
},
});
You can also use a wildcard for reading the state as well:
export default connect(MyComponent, {
name: 'myComponent',
state: {
list: [
{ name: 'feeb' },
{ name: 'foo' },
{ name: 'fiz' },
],
},
actions: {
logAllNames() {
const names = this.getState('list.*.name');
console.log(names);
},
},
});
Mapper Function
If you need to dynamically calculate the new value of the state key based on the old value, use a mapper function:
export default createModule('foo', {
state: {
list: [
{ count: 151 },
{ count: 120 },
{ count: 2 },
],
},
actions: {
setMinimum() {
return {
'list.*.count': (oldValue) => {
if (oldValue < 50) return 50;
return oldValue;
},
};
},
},
});
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
connect(component, configuration)
Parameter | Type | Description |
---|
component | Class | Function | Reference to the class/function of the component to be connected to the store. |
configuration | Object | The configuration object for the component. |
The connect function connects a component to the Redux store and automatically injects four properties into the component props. These properties are state
, actions
, globalState
and dispatch
.
The state
prop represents the component state in the Redux store. The default value for the state is an empty object.
The actions
prop is a list of action dispatcher functions that correspond to the actions that were defined in the configuration object. The default value for the actions prop is an empty object.
The globalState
prop represents the states of other connected components. The default value for the global state is an empty object.
The dispatch
prop represents a function that can be used to dispatch any action.
Example:
import React from 'react';
import { connect } from 'speedux';
const MyComponent = ({ state, actions, globalState, dispatch }) => {
console.log(state);
console.log(actions);
console.log(globalState);
console.log(dispatch);
return <div>...</div>;
};
export default connect(MyComponent, {
name: 'myComponent',
state: {
value: 'abc',
},
globalState: {
foo: 'fooComponent.some.value'
},
actions: {
setValue(newValue) {
return { value: newValue };
},
},
});
useReducer(key, reducer)
Allows registering a reducer function that can listen to any action dispatched by the store.
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));
dispatch(actionType, payload)
The dispatch
function is automatically injected into the props of a connected component and lets you dispatch any action and specify the action payload as well.
Parameter | Type | Description |
---|
actionType | String | Type of the action to be dispatched. |
payload | Object | Action payload object. |
See Dispatching Actions for an example.
The Configuration Object
The configuration object may contain one or more of the following keys:
name (String)
The name
key is the only required key in the configuration object. It must be unique for each component as it is used to identify the Redux state and actions for the component.
state (Object)
Represents the component state (or initial state) in the Redux store. If not provided, an empty object will be used as the component initial state.
The component state can only be updated by returning objects from action or handler functions. (explained below)
actions (Object)
A list of all the actions that may need to be dispatched from the component 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 props.actions.addNumbers()
is called.
The function should return an object that specifies the state keys that need to be updated and their new values.
const config = {
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 actions that the component needs to listen to and update its state accordingly. Provide the action type as the key and the handler function as the value. You can listen to actions dispatched by other components or 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.
globalState (Object)
The globalState
key allows reading states of other connected components. Simply provide an object with the name as the key and the state query as the value.
For example, if we have a connected component Cart
:
export default connect(Cart, {
name: 'shoppingCart',
state: {
items: [
{ id: 123, price: 12 },
{ id: 456, price: 34 },
{ id: 789, price: 56 },
],
totalCost: 102,
discountCode: 'u324y32',
},
...
});
And another connected component Checkout
that needs to read items inside the state of the Cart
component:
const Checkout = ({ globalState }) => {
console.log(globalState.cartItems);
console.log(globalState.cartItemPrices);
...
};
export default connect(Checkout, {
name: 'checkout',
globalState: {
cartItems: 'shoppingCart.items',
cartItemPrices: 'shoppingCart.items.*.price',
},
...
});
stateKey (String)
The stateKey
is used as a property name when the related Redux state object is injected into the component props. The default value is 'state'.
actionsKey (String)
The actionsKey
is used as a property name when the action creator functions object is injected into the component props. The default value is 'actions'.
globalStateKey (String)
The globalStateKey
is used as a property name when other component states are injected into the component props. The default value is 'globalState'.
dispatchKey (String)
The dispatchKey
is used as a property name when the dispatch
function is injected into the component props. The default value is 'dispatch'.
Hooks
React's hooks let you use state, execute side effects and use other React features without writing a class.
Speedux provides a set of hook APIs as an alternative to the existing connect()
Higher Order Component. These APIs allow you to subscribe to the Redux store, handle and dispatch actions, without having to wrap your components in connect()
.
Here's a quick example to demonstrate how to use them. First, you need to import createHooks
function then use it to get access to the other hooks. Remember that the name you pass to createHooks
hook must be unique to that component. Alternatively, you can pass a configuration object to createHooks
.
import React from 'react';
import { createHooks } from 'speedux';
const {
useState,
useActions,
useHandlers,
useDispatch,
useGlobalState,
} = createHooks('counter');
export default () => {
const state = useState(...);
const actions = useActions(...);
const dispatch = useDispatch();
const globalState = useGlobalState(...);
useHandlers(...);
return (
<div>
...
</div>
)
}
License
MIT