When it comes to Redux, the most we think of is the React-redux library, but in fact, Redux and React-redux are not the same thing. Redux is an architectural pattern, originating from Flux. React-redux is a concrete implementation of the combination of Redux ideas and React.
When we use React, we often encounter situations where components are deeply nested and value passing needs to be passed. It is obviously very painful to use props to pass values. To solve this problem, React provides us with a native context API, but the most common solution we use is to use React-redux, a library encapsulated based on the context API.
This article does not introduce the specific usage of React-redux, but uses a small example to understand what redux is.
OK, now let's get back to the point and implement our own redux.
1. Initial
First, we use create-react-app to create a project, delete the redundant part under src, only retain , and modify the DOM structure:
# <div > <div ></div> <div ></div> </div>
We create an object in it, use it to store and manage the data state of our entire application, and render the data on the page with a rendering function:
const appState = { head: { text: 'I'm the head', color: 'red' }, body: { text: 'I'm a body', color: 'green' } } function renderHead (state){ const head = ('head') = ; = ; } function renderBody (state){ const body = ('body') = ; = ; } function renderApp (state){ renderHead(state); renderBody(state); } renderApp(appState);
At this time, run the code and open the page, we can see that the red font has already appeared in the head, and the green font has appeared in the body.
If we think of head and body as two components in root, then we have implemented a globally unique state. This state is shared globally and can be called everywhere.
We can modify the rendering function of the head to see the following effect:
function renderHead (state){ const head = ('head') = + '--' + ; = ; = 'I'm a head modified body'; }
We see that in the head rendering function, we can not only use the value of the body attribute, but also change its value. This has a serious problem, because state is shared globally. Once the value of state is changed in one place, all components that use this value will be affected, and this change is unpredictable. Obviously, it adds difficulty coefficients to our code debugging, which is something we don’t want to see!
2. dispatch
Now it seems that there is a contradiction in front of us: we need data sharing, but the shared data being arbitrarily modified will cause unpredictable problems!
In order to resolve this contradiction, we need a butler who specifically manages the state of shared data. Any operation on shared data must be completed through him. In this way, we avoid the unpredictable harm caused by arbitrary modification of shared data!
We redefine a function and use this function as our steward to manage our shared data:
function dispatch(state, action) { switch () { case 'HEAD_COLOR': = break case 'BODY_TEXT': = break default: break } }
Let's re-modify the head's rendering function:
function renderHead (state){ const head = ('head') = + '--' + ; = ; dispatch(state, { type: 'BODY_TEXT', text: 'I am the head body modified by calling dispatch' }) }
The dispatch function receives two parameters, one is the state that needs to be modified, and the other is the modified value. At this time, although we still modify state, through the dispatch function, we make this change controllable, because we can find the source of the change in dispatch in any behavior that changes state.
In this way, we seem to have resolved the previous contradiction, we have created a global shared data, and strictly controlled any behavior that changes this data.
However, in a file, we have to save state and maintain the housekeeper function dispatch. As the application becomes more and more complex, this file will inevitably become lengthy and complicated and difficult to maintain.
Now, we separate state and dispatch:
- Save the state separately with a file
- Use another file to save the comparison relationship of state in dispatch changeState
- Finally, use a file to combine them and generate a globally unique store
In this way, not only does a single file become more streamlined, but in other applications, we can also easily reuse our method. We only need to pass in the state of different applications and modify the corresponding logic of stateChange, and we can safely perform various operations on the data by calling the dispatch method:Refer to the detailed answers to the front-end handwritten interview questions
# Change our directory structure and add a redux folder+ src ++ redux --- //Storing application data status--- // Maintain a set of logic to modify the store, only responsible for calculation, return to the new store--- // Create a store in combination with state and stateChange to facilitate any application reference-- ## Modified files# -- Global statusexport const state = { head: { text: 'I'm the head', color: 'red' }, body: { text: 'I'm a body', color: 'green' } } # -- Only responsible for calculation and modify storeexport const storeChange = (store, action) => { switch () { case 'HEAD_COLOR': = break case 'BODY_TEXT': = break default: break } } # -- Create a global storeexport const createStore = (state, storeChange) => { const store = state || {}; const dispatch = (action) => storeChange(store, action); return { store, dispatch } } # import { state } from './redux/'; import { storeChange } from './redux/'; import { createStore } from './redux/'; const { store, dispatch } = createStore(state, storeChange) function renderHead (state){ const head = ('head') = ; = ; } function renderBody (state){ const body = ('body') = ; = ; } function renderApp (store){ renderHead(); renderBody(); } // First renderingrenderApp(store);
Through the above file splitting, we see that not only makes a single file more streamlined, but the functions of the file are also clearer:
- In state, we save only our shared data
- In storeChange, we maintain the corresponding logic of changing the store and calculate the new store
- In createStore we create store
- In
3. Subscribe
Everything seems so beautiful, but when we call dispatch after the first rendering, we modify it
When the store is stored, we found that although the data has been changed, the page has not been refreshed. Only after the dispatch changes the data and re-calling renderApp() can the page be refreshed.
// First renderingrenderApp(store); dispatch({ type: 'BODY_TEXT', text: 'I called dispatch to modify the body' }) // After modifying the data, the page is not automatically refreshedrenderApp(store); // Recall renderApp Page refresh
This obviously does not meet our expectations. We do not want to refresh the page manually after changing the data. If we can automatically refresh the page after changing the data, of course it would be better!
It is obviously not appropriate to write renderApp directly in dispatch, so our createStore loses its universality.
We can add a collection array to createStore, collect the methods that need to be executed after dispatch call, and then execute them in a loop. This ensures the universality of createStore:
# createStore export const createStore = (state, storeChange) => { const listeners = []; const store = state || {}; const subscribe = (listen) => (listen); const dispatch = (action) => { storeChange(store, action); (item => { item(store); }) }; return { store, dispatch, subscribe } } # ··· const { store, dispatch, subscribe } = createStore(state, storeChange) ··· ··· // Add listenerssubscribe((store) => renderApp(store)); renderApp(store); dispatch({ type: 'BODY_TEXT', text: 'I called dispatch to modify the body' });
This way, every time we call dispatch, the page will be refreshed. If we don't want to refresh the page, we just want to alert in one sentence, just change the added listeners:
subscribe((store) => alert('The page has been refreshed')); renderApp(store); dispatch({ type: 'BODY_TEXT', text: 'I called dispatch to modify the body' });
In this way, we ensure the universality of createStore.
4. Optimization
At this point, we seem to have achieved the effect we wanted to achieve before: we have implemented a global public store, and the modification of this store is strictly controlled, and the page can be automatically refreshed after each modification of the store through dispatch.
However, obviously this is not enough. The above code is still a bit simple and has serious performance problems.
Although we only modified the body's copy, the head is also rendered again when the page is re-rendered. So, can we compare the old and new stores when rendering the page to perceive which parts need to be re-rendered and which parts do not need to be rendered again?
Based on the above idea, let's modify our code again:
# export const storeChange = (store, action) => { switch () { case 'HEAD_COLOR': return { ...store, head: { ..., color: } } case 'BODY_TEXT': return { ...store, body: { ..., text: } } default: return { ...store } } } # export const createStore = (state, storeChange) => { const listeners = []; let store = state || {}; const subscribe = (listen) => (listen); const dispatch = (action) => { const newStore = storeChange(store, action); (item => { item(newStore, store); }) store = newStore; }; return { store, dispatch, subscribe } } # import { state } from './redux/'; import { storeChange } from './redux/'; import { createStore } from './redux/'; const { store, dispatch, subscribe } = createStore(state, storeChange); function renderHead (state){ ('render head'); const head = ('head') = ; = ; } function renderBody (state){ ('render body'); const body = ('body') = ; = ; } function renderApp (store, oldStore={}){ if(store === oldStore) return; !== && renderHead(); !== && renderBody(); ('render app',store, oldStore); } // First renderingsubscribe((store, oldStore) => renderApp(store, oldStore)); renderApp(store); dispatch({ type: 'BODY_TEXT', text: 'I called dispatch to modify the body' });
As a result, we modified storeChange so that it no longer directly modify the original store, but instead returns a new store through calculation. We have modified the cearteStore to receive the new store returned by storeChange. After the dispatch is modified and the page is refreshed, we assign the new store to the previous store. When the page is refreshed, we will compare newStore and oldStore to perceive the parts that need to be re-rendered and complete some performance optimizations.
at last
Let’s take a brief look at redux through simple code examples. Although the code is still a bit simple, we have implemented several core concepts of redux:
- All states in the application are stored in a single store as an object tree.
- The only way to change the store is to trigger an action, which is an abstraction of action behavior.
This is the end of this article about the step-by-step explanation of the React handwritten redux process. For more related React redux content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!