Redux Simple as a rake
I have already had the opportunity to look into the redux library repository , but from somewhere there was an idea to go deeper into its implementation. In a way, I’d like to share my shocking or even disappointing discovery with the community.
TL; DR: the redux basic logic is placed in 7 lines of JS code.
About redux in brief (free translation of the github header):
scissors :
And now let's sort out what's left
All redux basic functionality fits into a tiny file for which hardly anyone will create a github repository :)
Everything. Yes, seriously, EVERYTHING .
This is how redux works. 18 pages of vacancies on HeadHunter with the search query “redux” - people who hope that you will understand in 7 lines of code. Everything else is syntactic sugar.
With these 7 lines, you can write TodoApp. Or whatever. But we will quickly rewrite the TodoApp from the documentation for redux.
Already at this stage I thought to throw the microphone from the stage and leave, but show must go on .
Let's see how the method works.
This is a method that allows instead of creating one huge reducer for the entire state of an application at once, breaking it up into separate modules.
It is used like this:
Further use of this store can be the same as the previous one.
The difference of my example and described in the same documentation for TodoApp is quite funny.
The documentation uses the mods syntax from ES6 (7/8 / ∞):
and accordingly rename todoReducer to todos and counterReducer to counter. And many in their code do the same. As a result, there is no difference, but for a person who is familiar with redux, from the first time this thing looks like magic, because the key of the state part (state.todos) corresponds to the function, which is also named only by the developer (function todos () {}) .
If we had to write such functionality on our micro-redux, we would do this:
This code does not scale well. If we have 2 “sub-states”, we need to write (state, action) twice , and good programmers don’t do that, right?
We have added to our cub redux 9 more lines and a lot of convenience.
Let's move on to another important feature that seems too complicated to pass by.
middleware in terms of redux is some kind of thing that listens to all dispatch and does something under certain conditions . Logs, plays sounds, makes requests to the server, ... - something .
In the original code, the middleware is passed as additional parameters to createStore, but if you don’t spare an extra line of code, then the use of this functionality looks like this:
In this case, the implementation of the applyMiddleware method, when you spend 10 minutes tinkering in someone else's code, comes down to a very simple thing: createStore returns an object with a “dispatch” field. dispatch, as we remember (do not remember) from the first listing of the code, is a function that only applies the reducer to our current state (newState = reducer (state, action)).
So, applyMiddleware does not override the dispatch method , adding some custom logic before (or after) updating the state.
Take, for example, the most popular middleware from the creators of redux - redux-thunk
Its meaning boils down to what can be done not only
but also pass complex functions to store.dispatch
And now, when we execute the command
then:
So, I climbed into the redux-thunk repository, and did the same thing as with redux - deleted comments and parameters that extend the basic functionality, but do not change the main one. The
following happened:
I understand that the design
Let me remind you that the original createStore method looked like this
That is, he accepted the attributes (reducer, initialState) and returned an object with the keys {dispatch, getState}.
It turned out that implementing the applyMiddleware method is easier than understanding how it works.
We take the already implemented createStore method and override its return value:
Under the hood redux contains very simple logical operations. Operations at the level "If gasoline in the cylinder lights up, the pressure increases." But then, can you build on these concepts a Formula 1 car - decide for yourself.
To add a simplified store.subscribe method to my “micro-redux”, it took 8 lines of code. And you?
TL; DR: the redux basic logic is placed in 7 lines of JS code.
About redux in brief (free translation of the github header):
Redux is a state management library for applications written in JavaScript.I cloned the redux repository , opened the source folder in the editor (ignoring docs , examples , etc.) and took the Delete key to the
It helps to write applications that behave stably / predictably, work on different environments (client / server / native code) and are easily testable.
- Removed all comments from the code.
Each library method is documented with JSDoc in great detail. - Removed the validation and error logging.
In each method, the input parameters are tightly controlled with very nice looking detailed comments in the console. - Removed bindActionCreators , subscribe , replaceReducer, and observable methods .
... because I could. Well, or because I was too lazy to write examples for them. But without corner cases, they are even less interesting than what awaits you ahead.
And now let's sort out what's left
We write redux for 7 lines
All redux basic functionality fits into a tiny file for which hardly anyone will create a github repository :)
functioncreateStore(reducer, initialState) {
let state = initialState
return {
dispatch: action => { state = reducer(state, action) },
getState: () => state,
}
}
Everything. Yes, seriously, EVERYTHING .
This is how redux works. 18 pages of vacancies on HeadHunter with the search query “redux” - people who hope that you will understand in 7 lines of code. Everything else is syntactic sugar.
With these 7 lines, you can write TodoApp. Or whatever. But we will quickly rewrite the TodoApp from the documentation for redux.
// Инициализация хранилищаfunctiontodosReducer(state, action) {
switch (action.type) {
case'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
]
case'TOGGLE_TODO':
return state.map(todo => {
if (todo.id === action.id) {
return { ...todo, completed: !todo.completed }
}
return todo
})
default:
return state
}
}
const initialTodos = []
const store = createStore(todosReducer, initialTodos)
// Использование
store.dispatch({
type: 'ADD_TODO',
id: 1,
text: 'Понять насколько redux прост'
})
store.getState()
// [{ id: 1, text: 'Понять насколько redux прост', completed: false }]
store.dispatch({
type: 'TOGGLE_TODO',
id: 1
})
store.getState()
// [{ id: 1, text: 'Понять насколько redux прост', completed: true }]
Already at this stage I thought to throw the microphone from the stage and leave, but show must go on .
Let's see how the method works.
combineReducers
This is a method that allows instead of creating one huge reducer for the entire state of an application at once, breaking it up into separate modules.
It is used like this:
// здесь мы переиспользуем метод todosReducer из прошлого примераfunctioncounterReducer(state, action) {
if (action.type === 'ADD') {
return state + 1
} else {
return state
}
}
const reducer = combineReducers({
todoState: todoReducer,
counterState: counterReducer
})
const initialState = {
todoState: [],
counterState: 0,
}
const store = createStore(reducer, initialState)
Further use of this store can be the same as the previous one.
The difference of my example and described in the same documentation for TodoApp is quite funny.
The documentation uses the mods syntax from ES6 (7/8 / ∞):
const reducer = combineReducers({ todos, counter })
and accordingly rename todoReducer to todos and counterReducer to counter. And many in their code do the same. As a result, there is no difference, but for a person who is familiar with redux, from the first time this thing looks like magic, because the key of the state part (state.todos) corresponds to the function, which is also named only by the developer (function todos () {}) .
If we had to write such functionality on our micro-redux, we would do this:
functionreducer(state, action) {
return {
todoState: todoReducer(state, action),
counterState: counterReducer(state, action),
}
}
This code does not scale well. If we have 2 “sub-states”, we need to write (state, action) twice , and good programmers don’t do that, right?
In the following example, you are expected not to be afraid of the Object.entries method and the Restructuring of the function parametersHowever, the implementation of the combineReducers method is quite simple (I remind you, this is if you remove validation and error output) and just a little refactor your own taste :
functioncombineReducers(reducersMap) {
returnfunctioncombinationReducer(state, action) {
const nextState = {}
Object.entries(reducersMap).forEach(([key, reducer]) => {
nextState[key] = reducer(state[key], action)
})
return nextState
}
}
We have added to our cub redux 9 more lines and a lot of convenience.
Let's move on to another important feature that seems too complicated to pass by.
applyMiddleware
middleware in terms of redux is some kind of thing that listens to all dispatch and does something under certain conditions . Logs, plays sounds, makes requests to the server, ... - something .
In the original code, the middleware is passed as additional parameters to createStore, but if you don’t spare an extra line of code, then the use of this functionality looks like this:
const createStoreWithMiddleware = applyMiddleware(someMiddleware)(createStore)
const store = createStoreWithMiddleware(reducer, initialState)
In this case, the implementation of the applyMiddleware method, when you spend 10 minutes tinkering in someone else's code, comes down to a very simple thing: createStore returns an object with a “dispatch” field. dispatch, as we remember (do not remember) from the first listing of the code, is a function that only applies the reducer to our current state (newState = reducer (state, action)).
So, applyMiddleware does not override the dispatch method , adding some custom logic before (or after) updating the state.
Take, for example, the most popular middleware from the creators of redux - redux-thunk
Its meaning boils down to what can be done not only
store.dispatch({type: 'SOME_ACTION_TYPE', some_useful_data: 1 })
but also pass complex functions to store.dispatch
functionsomeStrangeAction() {
returnasyncfunction(dispatch, getState) {
if(getState().counterState % 2) {
dispatch({
type: 'ADD',
})
}
awaitnewPromise(resolve => setTimeout(resolve, 1000))
dispatch({
type: 'TOGGLE_TODO',
id: 1
})
}
}
And now, when we execute the command
dispatch(someStrangeAction())
then:
- if the value of store.getState (). counterState is not divisible by 2, it will increase by 1
- a second after calling our method, todo with id = 1 will switch the completed true to false or vice versa.
So, I climbed into the redux-thunk repository, and did the same thing as with redux - deleted comments and parameters that extend the basic functionality, but do not change the main one. The
following happened:
const thunk = store => dispatch => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState)
}
return dispatch(action)
}
I understand that the design
const thunk = store => dispatch => actionIt looks scary, but it also just needs to be called a couple of times with arbitrary parameters and you realize that everything is not so scary, it's just a function that returns a function, a return function (okay, I agree, scary)
Let me remind you that the original createStore method looked like this
functioncreateStore(reducer, initialState) {
let state = initialState
return {
dispatch: action => { state = reducer(state, action) },
getState: () => state,
}
}
That is, he accepted the attributes (reducer, initialState) and returned an object with the keys {dispatch, getState}.
It turned out that implementing the applyMiddleware method is easier than understanding how it works.
We take the already implemented createStore method and override its return value:
functionapplyMiddleware(middleware) {
returnfunctioncreateStoreWithMiddleware(createStore) {
return(reducer, state) => {
const store = createStore(reducer, state)
return {
dispatch: action => middleware(store)(store.dispatch)(action),
getState: store.getState,
}
}
}
}
Conclusion
Under the hood redux contains very simple logical operations. Operations at the level "If gasoline in the cylinder lights up, the pressure increases." But then, can you build on these concepts a Formula 1 car - decide for yourself.
PS
To add a simplified store.subscribe method to my “micro-redux”, it took 8 lines of code. And you?