A lightweight state container based on Redux
Read the intro blog post
To install the stable version:
npm i redux-zero
This assumes that you’re using npm with a module bundler like webpack
ES2015+:
import createStore from "redux-zero";
import { Provider, connect } from "redux-zero/react";
TypeScript:
import * as createStore from "redux-zero";
import { Provider, connect } from "redux-zero/react";
CommonJS:
const createStore = require("redux-zero");
const { Provider, connect } = require("redux-zero/react");
UMD:
<!-- the store -->
<script src="https://unpkg.com/redux-zero/dist/redux-zero.min.js"></script>
<!-- for react -->
<script src="https://unpkg.com/redux-zero/react/index.min.js"></script>
<!-- for preact -->
<script src="https://unpkg.com/redux-zero/preact/index.min.js"></script>
<!-- for vue -->
<script src="https://unpkg.com/redux-zero/vue/index.min.js"></script>
<!-- for svelte -->
<script src="https://unpkg.com/redux-zero/svelte/index.min.js"></script>
Let's make an increment/decrement simple application with React:
First, create your store. This is where your application state will live:
/* store.js */
import createStore from "redux-zero";
const initialState = { count: 1 };
const store = createStore(initialState);
export default store;
Then, create your actions. This is where you change the state from your store:
/* actions.js */
const actions = store => ({
increment: state => ({ count: state.count + 1 }),
decrement: state => ({ count: state.count - 1 })
});
export default actions;
By the way, because the actions are bound to the store, they are just pure functions :)
Now create your component. With Redux Zero your component can focus 100% on the UI and just call the actions that will automatically update the state:
/* Counter.js */
import React from "react";
import { connect } from "redux-zero/react";
import actions from "./actions";
const mapToProps = ({ count }) => ({ count });
export default connect(
mapToProps,
actions
)(({ count, increment, decrement }) => (
<div>
<h1>{count}</h1>
<div>
<button onClick={decrement}>decrement</button>
<button onClick={increment}>increment</button>
</div>
</div>
));
Last but not least, plug the whole thing in your index file:
/* index.js */
import React from "react";
import { render } from "react-dom";
import { Provider } from "redux-zero/react";
import store from "./store";
import Counter from "./Counter";
const App = () => (
<Provider store={store}>
<Counter />
</Provider>
);
render(<App />, document.getElementById("root"));
Here's the full version: https://codesandbox.io/s/n5orzr5mxj
By the way, you can also reset the state of the store anytime by simply doing this:
import store from "./store";
store.reset();
There are three gotchas with Redux Zero's actions:
- Passing arguments
- Combining actions
- Binding actions outside your application scope
Here's how you can pass arguments to actions:
const Component = ({ count, incrementOf }) => (
<h1 onClick={() => incrementOf(10)}>{count}</h1>
);
const mapToProps = ({ count }) => ({ count });
const actions = store => ({
incrementOf: (state, value) => ({ count: state.count + value })
});
const ConnectedComponent = connect(
mapToProps,
actions
)(Component);
const App = () => (
<Provider store={store}>
<ConnectedComponent />
</Provider>
);
The initial component props are passed to the actions creator.
const Component = ({ count, increment }) => (
<h1 onClick={() => increment()}>{count}</h1>
);
const mapToProps = ({ count }) => ({ count });
const actions = (store, ownProps) => ({
increment: state => ({ count: state.count + ownProps.value })
});
const ConnectedComponent = connect(
mapToProps,
actions
)(Component);
const App = () => (
<Provider store={store}>
<ConnectedComponent value={10} />
</Provider>
);
There's an utility function to combine actions on Redux Zero:
import { connect } from "redux-zero/react";
import { combineActions } from "redux-zero/utils";
import Component from "./Component";
import firstActions from "../../actions/firstActions";
import secondActions from "../../actions/secondActions";
export default connect(
({ params, moreParams }) => ({ params, moreParams }),
combineActions(firstActions, secondActions)
)(Component);
If you need to bind the actions to an external listener outside the application scope, here's a simple way to do it:
On this example we listen to push notifications that sends data to our React Native app.
import firebase from "react-native-firebase";
import { bindActions } from "redux-zero/utils";
import store from "../store";
import actions from "../actions";
const messaging = firebase.messaging();
const boundActions = bindActions(actions, store);
messaging.onMessage(payload => {
boundActions.saveMessage(payload);
});
Async actions in Redux Zero are almost as simple as sync ones. Here's an example:
const mapActions = ({ setState }) => ({
getTodos() {
setState({ loading: true });
return client
.get("/todos")
.then(payload => ({ payload, loading: false }))
.catch(error => ({ error, loading: false }));
}
});
They're still pure functions. You'll need to invoke setState
if you have a loading status. But at the end, it's the same, just return whatever the updated state that you want.
And here's how easy it is to test this:
describe("todo actions", () => {
let actions, store, listener, unsubscribe;
beforeEach(() => {
store = createStore();
actions = getActions(store);
listener = jest.fn();
unsubscribe = store.subscribe(listener);
});
it("should fetch todos", () => {
nock("http://someapi.com/")
.get("/todos")
.reply(200, { id: 1, title: "test stuff" });
return actions.getTodos().then(() => {
const [LOADING_STATE, SUCCESS_STATE] = listener.mock.calls.map(
([call]) => call
);
expect(LOADING_STATE.loading).toBe(true);
expect(SUCCESS_STATE.payload).toEqual({ id: 1, title: "test stuff" });
expect(SUCCESS_STATE.loading).toBe(false);
});
});
});
The method signature for the middleware was inspired by redux. The main difference is that action is just a function:
/* store.js */
import createStore from "redux-zero";
import { applyMiddleware } from "redux-zero/middleware";
const logger = store => (next, args) => action => {
console.log("current state", store.getState());
console.log("action", action.name, ...args);
return next(action);
};
const initialState = { count: 1 };
const middlewares = applyMiddleware(logger, anotherMiddleware);
const store = createStore(initialState, middlewares);
export default store;
You can setup DevTools middleware in store.js to connect with Redux DevTools and inspect states in the store.
/* store.js */
import createStore from "redux-zero";
import { applyMiddleware } from "redux-zero/middleware";
import { connect } from "redux-zero/devtools";
const initialState = { count: 1 };
const middlewares = connect ? applyMiddleware(connect(initialState)) : [];
const store = createStore(initialState, middlewares);
export default store;
Also, these are unofficial tools, maintained by the community:
- Redux-Zero Tools
- redux-zero persist middleware
- redux-zero logger middleware
- redux loading middleware
You can use the BoundActions
type to write your React component props in a type
safe way. Example:
import { BoundActions } from "redux-zero/types/Actions";
interface State {
loading: boolean;
}
const actions = (store, ownProps) => ({
setLoading: (state, loading: boolean) => ({ loading })
});
interface ComponentProps {
value: string;
}
interface StoreProps {
loading: boolean;
}
type Props = ComponentProps & StoreProps & BoundActions<State, typeof actions>
const Component = (props: Props) => (
<h1 onClick={() => props.setLoading(!props.loading)}>{props.value}</h1>
);
const mapToProps = (state: State): StoreProps => ({ loading: state.loading });
const ConnectedComponent = connect<State, ComponentProps>(
mapToProps,
actions
)(Component);
const App = () => (
<Provider store={store}>
<ConnectedComponent value={10} />
</Provider>
);
By doing this, TypeScript will know the available actions and their types
available on the component's props. For example, you will get a compiler error if you
call props.setLoding
(that action doesn't exist), or if you call it
with incorrect argument types, like props.setLoading(123)
.
Redux Zero was based on this gist by @developit
- Make sure all bindings are working for latest versions of React, Vue, Preact and Svelte
- Add time travel
Help is needed for both of these