If you are still not decided which state management library choose for your project, you can find below a short comparison of two mainly used ones : MobX, Redux.
Please note that this is not a tutorial intended to discover the libraries, and requires basic knowledge of javascript and analysed tools.
At the time of writing this document, MobX and Redux are the most popular state management frameworks. You can use MobX/Redux with vanilla Javascript, React or even Angular - there exist bindings for each library to make it work with those store management tools. Enclosed app example is an implementation of both with React, and gives a snapshot of complexity and structure you need to put in place to run your project (to see it in action, jump directly to Usage part). The analysis also takes into consideration Redux Toolkit, as new recommended way to write Redux logic.
MobX | Redux | Redux Toolkit | |
---|---|---|---|
Release 1.0 | 13 Oct 2015 | 14 Aug 2015 | 23 Oct 2019 |
Learning curve | shallow | steep | steep |
Verbosity | low | high | low |
Structure | simple | complex | simple |
Multiple stores | yes | no | no |
Mutability | mutable | immutable | immutable |
Debugging | average | excellent | excellent |
Debugging - comparison based on daily development and tests done with 'Redux DevTools' and 'MobX Developer Tools' (extensions for Chrome).
MobX | Redux | Redux Toolkit |
---|---|---|
https://github.com/mobxjs/mobx | https://github.com/reduxjs/redux | https://github.com/reduxjs/redux-toolkit |
Integration with React:
yarn add mobx
yarn add mobx-react
yarn add @babel/plugin-proposal-decorators --dev
: dependency required to use decorators @observable, @computed, @action, @inject etc.More about: https://babeljs.io/docs/en/babel-plugin-proposal-decorators https://mobx.js.org/enabling-decorators.html
yarn add redux
yarn add react-redux
yarn add @reduxjs/toolkit
yarn add react-redux
More about add-ons:
mobx-react
: package with React component wrapper for combining React with MobXMore about: https://github.com/mobxjs/mobx-react
react-redux
: official React bindings for ReduxMore about: https://react-redux.js.org/
Before exploring MobX and Redux, here is a dictionary of common terms:
- Store - holds the state of the application
- Components - dumb/presentational components (discover state through props passed as params)
- Containers - components aware of the store existence and interacts with it
- Action - function that trigger the state update (contains Type attribute that is used in Reducer)
- Action creator - function that returns an action object
- Reducer - pure function that receives a state and action as arguments, copies the existing state and makes changes to the copied values (immutable update)
- Selector - function that computes derived data from the store
- Decorator - declaration that is used to modify class properties/methods
- Mutable - state of an object can be modified after object creation. In MobX state can be modified directly, mutating previous store value - example:
this.todos.push({id: id, text, completed: false});
- Immutable - immutability is a core principle in functional programming, saying that the object state cannot be altered (example: primitive data types such as booleans, numbers, strings are already immutable, and objects or arrays not). In Redux, that term means the state becomes each time a brand new object instead of mutating the old store value - example:
[
...state, { id: action.id, text: action.text, completed: false}
]
MobX gives possibility to define multiple stores and its state is mutable. MobX's world is implicit where observable properties and actions update the store.
//TodoStore.js
//Trackable property
@observable todos = [];
...
//Action that modifies the state
@action addTodo (text= 'DUMMY TODO') {
this.todos.push({id: nextTodoId++ , text, completed: false});
}
...
//Derived value, cached until observable state updates
@computed get uncompletedTodosCount() {
return this.todos.filter(todo => !todo.completed).length;
}
The store injection pattern used by the mobx-react makes easy linking the state to component:
//AddTodo.js
@inject('store')
class AddTodo extends Component {
...
onFinish = values => {
this.props.store.addTodo(values.todo);
...
};
}
UI updates automatically thanks to @observer
decorator that listens @observable
updates:
//TodoList.js
@inject('store')
@observer
class TodoList extends Component {
render() {
...
};
}
The core principles of Redux is only one store as the single source of truth.
The state is immutable which makes it more predictable. The structure of Redux is slightly more complex than in MobX.
Redux uses actions, action creators, reducers to update the data and requires much more effort to set it up initially.
Updates have to be tracked manually using subscribers (@computed variables in MobX) - subscriber triggers after the root reducer has returned a new state.
In Redux only way to update the store is to call an action. Example of an action creator - a factory that creates an action containing type and payload:
//actions/todo.js
export const addTodo = text => ({
type: types.ADD_TODO,
id: nextTodoId++,
text
})
...
Dispatching action to the store using dispatch
function from Redux to trigger store update:
//containers/AddTodo.js
const mapDispatchToProps = dispatch => ({
addTodo: todo => dispatch(addTodo(todo))
});
...
Reducer is listening the events and handles actions based on action type, calculating the new state value with payload arguments:
//reducers/AddTodo.js
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text ? action.text : 'DUMMY TODO',
completed: false
}
]
...
default:
return state
}
}
...
Selector computes derived data (using 'reselect' library) when state getTodos
updates:
More about: https://github.com/reduxjs/reselect
//selectors/index.js
import { createSelector } from 'reselect'
const getTodos = state => state.todos
export const getUncompletedTodosCount = createSelector(
[getTodos],
todos => (
todos.reduce((count, todo) =>
!todo.completed ? count + 1 : count,
0
)
)
)
...
In this example we didn't use Redux Thunk middleware that allows handle asynchronous requests. The middleware can be added as follows:
yarn add redux-thunk
More about: https://github.com/reduxjs/redux-thunk
As the time goes by, and one year means a century in web development world, a new package based on essential Redux features appeared in 2019, called Redux Toolkit.
Redux Toolkit is our official, opinionated, batteries-included toolset for efficient Redux development. https://redux.js.org/redux-toolkit
Redux Toolkit introduced slices, based on ducks modular pattern that holds reducers, action types and action creators inside one directory that represents the feature. Library includes among other things Redux, Reselect, Redux-thunk and immer. The tool cares of immutability itself making immutable changes with normal mutative code. Before using Redux Toolkit you still need to know basics of Redux. The concept replies simply on most common issues raised against Redux like:
- store configuration is complicate
- code is verbose
- requires complex folder structure
- no clear/best practices given to organize the code
The example of Slice (from the application you can lunch following Usage part) integrating action's and reducer's logic from Redux:
//features/todos/todoSlice.js
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
reducer(state, action) {
const { id, text = 'DUMMY TODO' } = action.payload
state.push({ id, text, completed: false })
},
prepare(text) {
return { payload: { text, id: nextTodoId++ } }
}
},
toggleTodo(state, action) {
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed
}
}
}
})
Folder structures from each Store management tool:
app/
├─ components/
├─ containers/
├─ stores/
├─ ...
app/
├─ actions/
├─ components/
├─ constants/
├─ containers/
├─ reducers/
├─ selectors/
├─ ...
app/
├─ components/
├─ features/
│ └─ feature_name
├─ ...
Debugging with 'MobX Developer Tools' and 'Redux DevTools' as extensions for Chrome:
Create a local repository and clone the code:
$ git clone https://github.com/KamilKubicki/react-mobx-redux-toolkit.git
Install dependencies:
$ yarn
Run the app:
$ yarn start
Navigating to https://localhost:3000 you should see the app main page.