Where to keep the state in 2020?
JavaScript community
Volodymyr Vyshko
Redux
Is it dead? Should we use it?
State management in general
State managers overview
Choosing the right one for your application
JavaScript community
JavaScript community
In general
User
Scene
Router
State
State
Allocated objects
All the variables
File descriptors
Network sockets
It is basically all of the information that represents what is currently happening in the application.
What is your opinion?
How data gets into/out of a store
How business logic interacts with the UI
Application architecture
How information flows through a system
Data persistence
Programming paradigms
networking and caching
Presentation behavior
lets call it
You have reasonable amounts of data changing over time
Many people working on the project, you want a consistent way to work with state
You don't want to reinvent the wheel again by your own
You want to use some extensions for easier debugging
If your state management doesn't need all the Redux features, use useState, useReducer and useContext. If your state management needs Redux as one global state container with middleware, introduce Redux
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
May cover all your needs!
come out 2015
Flux
Flux is not a state manager, but an architecture/pattern
Flux architecture
In functional programming, functions are not supposed to modify the external state. All that they are supposed to do - return a value based solely on their input.
const createStore = (reducer, listeners = []) => {
let state;
const getState = () => state;
const dispatch = (action) => {
if (typeof action === 'function') {
action(dispatch);
} else {
state = reducer(state, action);
listeners.forEach(listener => listener());
}
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
dispatch({});
return { getState, dispatch, subscribe };
};
Observer
Flux
"one-way" data flow
Redux
immutability,
reducer function composition
Multiple classes
Multiple stores
Single
store, but multiple reducers
54.5k
October 2019
October 2020
25%
50%
75%
100%
25%
Learning curve
Boilerplate code
Performance
Central store
No built-in way to handle side-effects
Comparing to other libraries
Is it dead ?
Use react features/hooks instead ?
Hi, I'm a Redux maintainer.
No, Context does not replace Redux, although there is some overlap. And yes, Redux will continue to be very relevant and useful for a long time. It's currently used by 50% of React apps, and lots of folks are continuing to learn it on a daily basis.
Mark Erikson
No.
Consistent architectural pattern
Debugging capabilities
Middleware
Addons and extensibility
Cross-platform and cross-framework usage
Better performance than Conext
import { createSlice } from '@reduxjs/toolkit'
const initialState = { value: 0 }
const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment(state) {
state.value++
},
decrement(state) {
state.value--
},
incrementByAmount(state, action) {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer
I would like to amend this: don’t use Redux until you have problems with vanilla React
Dan abramov
Don’t use Redux unless you *tried* local component state and were dissatisfied.
You don’t need Redux if your data never changes. The whole point of it is managing changes.
If your reducer looks “boring” don’t use Redux. It just adds unnecessary indirection in this case.
Don't use Redux if you wasting much CPU time or RAM resources on maintaining the immutability
Don't use redux if your application using it mainly for caching fetched resources
Don't use redux if your app mainly consists of complex forms
Don't use Redux if your containers are mostly independent
Should I keep everything in Redux store?
React state
Redux
You need a single source of truth
You want to maintain an undo history
You need a serializable state/actions
Travel between the state history in development
Provide alternative UI without disturbing too much of the business logic
Still good in many cases
Not a silver bullet, but
come out 2015 (later then Redux)
MobX
NPM downloads
22.6k
import { makeObservable, observable, action } from "mobx"
class Counter {
count = 0;
constructor() {
makeObservable(this, {
count: observable,
increase: action,
decrease: action
});
this.text = text
}
increase() {
this.count++;
}
decrease() {
this.count--;
}
}
Using decorators is no longer the norm in MobX 6
TLDR: In short, actions modify the state, which triggers reactions.
Declarative MVVM
By default, MobX uses proxies to make arrays and plain objects observable.
new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Отказано в доступе");
} else {
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
},
set(target, prop, val) { // перехватываем запись свойства
if (prop.startsWith('_')) {
throw new Error("Отказано в доступе");
} else {
target[prop] = val;
return true;
}
}
MobX
Team prefers OOP/Reactive paradigmas
Medium-sized projects
Lots of data and mutations
No IE support
Immutability
Tree model representation
Snapshots and "time traveling"
Powered by MobX
Opinionated
MST
"saga"- like built-in async actions
MST Dev tools
MST
const NoteModel = types.model({
text: types.string,
bg: types.maybe(
types.string
),
id: types.string,
checked: types.boolean,
}).actions(self => ({
toggle() {
self.checked = !self.checked
},
setText(text) {
self.text = text
},
setColor(color) {
self.bg = color;
}
}))
const RootStore = types.model({
items: types.array(NoteModel),
selected: types.maybeNull(
types.string
)
})
.views(self => ({
get selectedNote() {
return self.selected && self.items.find(i => i.id === self.selected)
}
}))
.actions(self => ({
add(text) {
self.items.push(NoteModel.create({ text, checked: false, id: uuid() }));
},
deleteChecked() {
self.items = self.items.filter(el => !el.checked);
},
setSelected(data) {
self.selected = data && data.id;
}
}));
const NotesApp = observer(({ data }) => (
<div className="App">
<NotesList
items={data.items}
onAdd={text => data.add(text)}
onDelete={() => data.deleteChecked()}
onSelect={item => data.setSelected(item)}
/>
<Stats items={data.items} />
<SelectedItemDrawer
selected={data.selectedNote}
onClose={() => data.setSelected(null)}
/>
</div>
));
const store = RootStore.create({
items: [
NoteModel.create({ text: 'Test test', id: uuid(), checked: false }),
NoteModel.create({ text: 'Test', id: uuid(), checked: false, bg: '#119900' }),
],
selected: null,
})
const Container = () => <NotesApp data={store} />
but...
there are some aspects you should know...
It works slower than redux on wide model trees. To solve the issue, you need to turn of the immutability of some nodes
It has some problems with TS integration and performance. Harder to debug than redux.
Less information on the internet, smaller community.
MST
come out 2017
Trends
13.3k
Imagine you are writing a UI for a plane control room
Engine 1: 50% power
Engine 2: 50% power
Flaps 1: straight
Flaps 2: 7% rotation
Total speed: 460kmp/h
Chassis: folded
A state machine is a finite set of states that can transition to each other deterministically due to events.
export const createTodoMachine = ({ id, title, completed }) =>
createMachine(
{
id: "todo",
initial: "reading",
context: {
id,
title,
prevTitle: title,
completed
},
on: {
TOGGLE_COMPLETE: {
actions: [
assign({ completed: true }),
sendParent((context) => ({ type: "TODO.COMMIT", todo: context }))
]
},
DELETE: "deleted"
},
states: {
reading: {
on: {
SET_COMPLETED: {
actions: [assign({ completed: true }), "commit"]
},
TOGGLE_COMPLETE: {
actions: [
assign({ completed: (context) => !context.completed }),
"commit"
]
},
SET_ACTIVE: {
actions: [assign({ completed: false }), "commit"]
},
EDIT: {
target: "editing",
actions: "focusInput"
}
}
},
editing: {
entry: assign({ prevTitle: (context) => context.title }),
on: {
CHANGE: {
actions: assign({
title: (_, event) => event.value
})
},
COMMIT: [
{
target: "reading",
actions: sendParent((context) => ({
type: "TODO.COMMIT",
todo: context
})),
cond: (context) => context.title.trim().length > 0
},
{ target: "deleted" }
],
BLUR: {
target: "reading",
actions: sendParent((context) => ({
type: "TODO.COMMIT",
todo: context
}))
},
CANCEL: {
target: "reading",
actions: assign({ title: (context) => context.prevTitle })
}
}
},
deleted: {
onEntry: sendParent((context) => ({
type: "TODO.DELETE",
id: context.id
}))
}
}
},
{
actions: {
commit: sendParent((context) => ({
type: "TODO.COMMIT",
todo: context
})),
focusInput: () => {}
}
}
);
XState has a visualizer: https://statecharts.github.io/xstate-viz which is feasible due to the declarative nature.
dev-tools
Async manipulations ?
const redditMachine = Machine({
/* ... */
context: {
subreddit: null,
posts: null
},
states: {
idle: {},
selected: {
initial: 'loading',
states: {
loading: {
invoke: {
id: 'fetch-subreddit',
src: invokeFetchSubreddit,
onDone: {
target: 'loaded',
actions: assign({
posts: (context, event) => event.data
})
},
onError: 'failed'
}
},
loaded: {},
failed: {}
}
}
},
on: {
/* ... */
}
});
function invokeFetchSubreddit(context) {
const { subreddit } = context;
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => json.data.children.map(child => child.data));
}
Use it to predict all application state transitions in advance; this will help you not trap into sudden logic corner cases.
Use it if you like state-machines :D
come out 2020 (Still experimental)
Recoil
Well, I know that on one tool we saw a 20x or so speedup compared to using Redux. This is because Redux is O(n) in that it has to ask each connected component whether it needs to re-render, whereas we can be O(1).
Dave McCabe
Recoil
Better performance on such apps
Multiple updates in different DOM subtrees, without updating a common parent
Recoil
Recoil defines a directed graph orthogonal to but also intrinsic and attached to your React tree.
Recoil
Recoil
Atoms contain the source of truth for our application state. In our todo-list, the source of truth will be an array of objects, with each object representing a todo item.
const todoListState = atom({
key: 'todoListState',
default: [],
});
function TodoList() {
const todoList = useRecoilValue(todoListState);
return (
<>
{/* <TodoListStats /> */}
{/* <TodoListFilters /> */}
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
}
Recoil
A selector represents a piece of derived state. You can think of derived state as the output of passing state to a pure function that modifies the given state in some way.
const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'Show All',
});
const filteredTodoListState = selector({
key: 'filteredTodoListState',
get: ({get}) => {
const filter = get(todoListFilterState);
const list = get(todoListState);
switch (filter) {
case 'Show Completed':
return list.filter((item) => item.isComplete);
case 'Show Uncompleted':
return list.filter((item) => !item.isComplete);
default:
return list;
}
},
});
But not a react one...
Recoil
Dynamically created state
Async Support
Code-splitting out of the box
Minimal boilerplate
Concurrent mode support
Performant
MobX
Recoil
vs
Mutable state
Immutable (by default)
General purpose
React only
More magic
More implicit specifications
Ready for prod
Experimental only
Recoil
come out 2016
+
Composable reactive state through Atoms and Lenses
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Atom, F } from '@grammarly/focal'
// application state
const state = Atom.create({
count: 0
})
// the counter component from before
const Counter = (props: { count: Atom<number> }) =>
<F.div>
Count: {props.count}.
<button onClick={() => props.count.modify(x => x + 1)}>
Click again!
</button>
</F.div>
// the app component which takes the whole app state from the
// `state` prop
const App = (props: { state: Atom<{ count: number }> }) =>
<div>
Hi, here's a counter:
{/*
this is where we take apart the app state and give only a part of it
to the counter component:
*/}
<Counter count={props.state.lens('count')} />
</div>
You want better performance then Redux, but concerned with MobX magic
You want to work at Grammarly
Your team likes a functional reactive approach
Built-in features into your view lib/framework
Apollo
Relay
MobX
Redux
MST
You don't know Redux => Redux 100%
You like FP & Reactive approach => Focal
You want to be in trend => Recoil
Many aspects of your project should be considered
JavaScript community
JavaScript community
Is redux dead - https://blog.isquaredsoftware.com/2018/03/redux-not-dead-yet/
https://medium.com/@dan_abramov/the-evolution-of-flux-frameworks-6c16ad26bb31
https://github.com/reduxjs/redux/issues/1385
https://medium.com/dailyjs/functional-js-3-state-89d8cc9ebc9e
https://medium.com/better-programming/react-state-management-in-2020-719d10c816bf
https://dev.to/alexandrzavalii/recoil-ideal-react-state-management-library-1203
Recoil vs MobX https://twitter.com/mweststrate/status/1261369871230787584
XState vs Redux
https://stackoverflow.com/questions/54482695/what-is-an-actual-difference-between-redux-and-a-state-machine-e-g-xstate