- Build Yourself a Redux
- By Justin Deal
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: Tanglie1993, Lsvih
- Proofread by: Nia3y, JohnieXu
Redux is a simple library that helps you manage the state of your JavaScript application. Although it’s simple, it’s easy to get stuck in the learning process. I often need to explain the use and principles of Redux, and I always start by explaining how to implement Redux. So here’s what we do: Start from scratch and write a Redux that works. Our implementation will not consider all cases, but will reveal most of the principles of Redux.
Notice that what we’re actually going to implement is Redux and React Redux. Here, we combine Redux with the famous UI library React, which is the most common combination in real-world scenarios. Even if you combine Redux with something else, everything here is pretty much the same.
Let’s get started!
Implement your own state object
Most applications get state from the server. We start by creating state locally (even if we get state from the server, we initialize the application with some state first). We’re going to build a simple notebook app so we don’t have TODO cookie-cutter TODO apps, and as we’ll see later, this notebook app also drives us TODO interesting things to control state.
const initialState = {
nextNoteId: 1.notes: {}};Copy the code
First, notice that our data is just a simple JS object. Redux helps us manage state changes, but it doesn’t care much about the state itself.
Why Redux?
Before we go any further, let’s take a look at how to develop the application we created without using Redux. First, we need to bind the initialState object to the window like this:
window.state = initialState;
Copy the code
This is our store! Now we don’t need Redux, let’s build a new note-taking component:
const onAddNote = () = > {
const id = window.state.nextNoteId;
window.state.notes[id] = {
id,
content: ' '
};
window.state.nextNoteId++;
renderApp();
};
const NoteApp = ({notes}) = > (
<div>
<ul className="note-list">
{
Object.keys(notes).map(id => (
// Obviously we should render something more interesting than the id.
<li className="note-list-item" key={id}>{id}</li>))}</ul>
<button className="editor-button" onClick={onAddNote}>New Note</button>
</div>
);
const renderApp = () = > {
ReactDOM.render(
<NoteApp notes={window.state.notes}/>.document.getElementById('root')); }; renderApp();Copy the code
You can try this example in JSFiddle.
const initialState = {
nextNoteId: 1.notes: {}};window.state = initialState;
const onAddNote = () = > {
const id = window.state.nextNoteId;
window.state.notes[id] = {
id,
content: ' '
};
window.state.nextNoteId++;
renderApp();
};
const NoteApp = ({notes}) = > (
<div>
<ul className="note-list">{object.keys (notes).map(id => (// obviously we need to show something more interesting than id.<li className="note-list-item" key={id}>{id}</li>))}</ul>
<button className="editor-button" onClick={onAddNote}>New Note</button>
</div>
);
const renderApp = () = > {
ReactDOM.render(
<NoteApp notes={window.state.notes}/>.document.getElementById('root')); }; renderApp();Copy the code
It’s not a very practical application, but it works. It looks like we’ve proved that notepad can be made without Redux, so this article is done
And no…
Let’s look ahead: we added some new features, developed a great server, started a company to sell it, got a lot of users, then added a lot of new features, made some money, expanded the company… (Too much thinking)
(It’s hard to see in this simple Notepad application.) On our way to success, the application will probably keep growing, with hundreds of components in hundreds of files. Our application will perform asynchronous operations, so we will have code like this:
const onAddNote = () = > {
window.state.onLoading = true;
renderApp();
api.createNote()
.then((note) = > {
window.state.onLoading = false;
window.state.notes[id] = note;
renderApp();
});
};
Copy the code
There are also bugs like this:
const ARCHIVE_TAG_ID = 0;
const onAddTag = (noteId, tagId) = > {
window.state.onLoading = true;
// Oops, I forgot to render here!
// When using a fast local server, we might not notice.
api.addTag(noteId, tagId)
.then(() = > {
window.state.onLoading = false;
window.state.tagMapping[tagId] = noteId;
if (ARCHIVE_TAG_ID) {
// Oops, some naming bugs. Probably due to rough search/replace. It wasn't until we tested the profile page that no one was actually using that we found out.
window.state.archived = window.state.archive || {};
window.state.archived[noteId] = window.state.notes[noteId];
delete window.state.notes[noteId];
}
renderApp();
});
};
Copy the code
And some weird, temporary state changes that almost no one knows what they do:
const SomeEvilComponent = () = > {
<button onClick={()= > window.state.pureEvil = true}>Do Evil</button>
};
Copy the code
Over a long period of time, many developers add code to a large code base together, and we will encounter a number of problems:
- Render can trigger anywhere. There will be strange UI freezes or freezes, and it will seem random.
- Potential race conditions exist even in the small amount of code we see.
- The state is almost too chaotic to test. You have to keep the entire application in a certain state, and then debug it to see if it works as you expect.
- If you find a bug, you can make an educated guess about where to fix it, but in the end, every line of code in your app is suspect.
This last point is the worst and the main reason we chose Redux. If you want to reduce the overall complexity of your application, it’s best (in my opinion) to limit how and where you can change the state of your application. Redux is not a panacea for other problems, but these limitations will make them less likely.
Reducer
So how does Redux provide these limitations and help you manage your state? Start with a simple function that inputs the current state and returns a new state. For our notebook application, if we provide an action to add notes, we should get a state after adding new notes:
const CREATE_NOTE = 'CREATE_NOTE';
const UPDATE_NOTE = 'UPDATE_NOTE';
const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE:
return // There is a new note status
case UPDATE_NOTE:
return // Update the status after the note
default:
return state
}
};
Copy the code
If you are not happy with the switch statement, you can also write reducer in another way. I often use an object with a key pointing to each type of handler, like this:
const handlers = {
[CREATE_NOTE]: (state, action) = > {
return // The new state of the new note
},
[UPDATE_NOTE]: (state, action) = > {
return // Modify the new state of the note}};const reducer = (state = initialState, action) = > {
if (handlers[action.type]) {
return handlers[action.type](state, action);
}
return state;
};
Copy the code
It doesn’t matter how you write it. Reducer is your self-written function and can be implemented in any way. Redux doesn’t care how you do it.
immutability
Redux is concerned that your Reducer must be a pure function. That means you should never write:
const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE: {
// Don't change your status like this!!
state.notes[state.nextNoteId] = {
id: state.nextNoteId,
content: ' '
};
state.nextNoteId++;
return state;
}
case UPDATE_NOTE: {
// Don't change your status like this!!
state.notes[action.id].content = action.content;
return state;
}
default:
returnstate; }};Copy the code
In fact, if you change states like this, Redux will not work properly. Because although you are changing state, the object reference does not change (the state of the component binding is the reference to the binding object), so your application will not update properly. It also prevents the use of some of the Redux developer tools because they track previous state. If you are constantly changing the state, you cannot do state rollback.
In principle, changing the state makes it more difficult to assemble your own Reducer (and possibly other parts of the application). Pure functions are predictable because they produce the same output with the same input. If you get into the habit of modifying your status, it’s all over. The function call becomes indeterminate. You have to keep the whole function call tree in mind.
This predictability comes at a high price, especially because JavaScript natively does not support immutable objects. In this example, we will be using native JavaScript and will need to write more redundant code. The following is the correct way to write reducer:
const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE: {
const id = state.nextNoteId;
const newNote = {
id,
content: ' '
};
return {
...state,
nextNoteId: id + 1.notes: {
...state.notes,
[id]: newNote
}
};
}
case UPDATE_NOTE: {
const {id, content} = action;
consteditedNote = { ... state.notes[id], content };return {
...state,
notes: {
...state.notes,
[id]: editedNote
}
};
}
default:
returnstate; }};Copy the code
I’m using object extension syntax (…) . If you want to use more traditional JavaScript syntax, you can use object.assign. The idea is the same: don’t change the state, but create shallow copies of any state, nested objects, arrays. For any immutable object, we refer only to the part that exists. Let’s take a closer look at this code:
return {
...state,
notes: {
...state.notes,
[id]: editedNote
}
};
Copy the code
We will only change the Notes property, and the state property will remain the same. . State means reuse existing properties. Similarly, in Notes, we only change the part we are editing… The rest of the state.notes will not change. This way, we can use shouldComponentUpdate or PureComponent to make sure that the components with unchanged Note as props are not rendered repeatedly. Remember, we also need to avoid saying reducer like this:
const reducer = (state = initialState, action) = > {
// Ok, we avoided modification, but... Don't do that!
state = _.cloneDeep(state)
switch (action.type) {
// ...
case UPDATE_NOTE: {
// Now you can make some changes
state.notes[action.id].content = action.content;
return state;
}
default:
returnstate; }};Copy the code
Again, you get concise code to modify objects, and Redux actually works fine in this case, but it won’t be optimized. Each object and array is brand new every time the state changes, so any components that depend on those objects and arrays will be rerendered, even if you haven’t actually made any changes to their state.
Our immutable Reducer must require more type definitions, and there will be higher learning costs. But later, you’ll be glad that the state-changing functions are independent and easy to test. For a real application, you might want to look at things like Lodash-fp, or Ramda or immutable.js. Here, we use a variant of immutability-Helper, which is very simple. Mind you, there’s a big hole here, and I even wrote a new library for it. Native JS is good, too, and has good and robust type definition solutions such as Flow and TypeScript. Make sure to use smaller-grained functions, just as you would with React: although it uses more code overall than jQuery, each component is more predictable.
Use our Reducer
Let’s add an action to our Reducer and generate a new state.
const state0 = reducer(undefined, {
type: CREATE_NOTE
});
Copy the code
State0 now looks like this:
{
nextNoteId: 2.notes: {
1: {
id: 1.content: ' '}}}Copy the code
Note that we use undefined as the input to the state. Redux always passes undefined as the initialState, and you generally need to use state = initialState to select the initialState object. Next time, Redux will enter the previous state.
const state1 = reducer(state0, {
type: UPDATE_NOTE,
id: 1.content: 'Hello, world! '
});
Copy the code
State1 now looks like this:
{
nextNoteId: 2.notes: {
1: {
id: 1.content: 'Hello, world! '}}}Copy the code
You can use our Reducer here (code link) :
const CREATE_NOTE = 'CREATE_NOTE';
const UPDATE_NOTE = 'UPDATE_NOTE';
const initialState = {
nextNoteId: 1.notes: {}};const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE: {
const id = state.nextNoteId;
const newNote = {
id,
content: ' '
};
return {
...state,
nextNoteId: id + 1.notes: {
...state.notes,
[id]: newNote
}
};
}
case UPDATE_NOTE: {
const {id, content} = action;
consteditedNote = { ... state.notes[id], content };return {
...state,
notes: {
...state.notes,
[id]: editedNote
}
};
}
default:
returnstate; }};const state0 = reducer(undefined, {
type: CREATE_NOTE
});
const state1 = reducer(state0, {
type: UPDATE_NOTE,
id: 1.content: 'Hello, world! '
});
ReactDOM.render(
<pre>{JSON.stringify(state1, null, 2)}</pre>.document.getElementById('root'));Copy the code
Of course, Redux doesn’t create more variables like this, but we’ll get to the actual implementation soon. The point is, the core of Redux is just a little piece of code you write, a function that simply takes one state and returns the next. Why is this function called reducer? Because it can be plugged into the standard Reduce function.
const actions = [
{type: CREATE_NOTE},
{type: UPDATE_NOTE, id: 1.content: 'Hello, world! '}];const state = actions.reduce(reducer, undefined);
Copy the code
State will then look the same as state1 before:
{
nextNoteId: 2.notes: {
1: {
id: 1.content: 'Hello, world! '}}}Copy the code
Here you can add elements to our Actions array and put them into the reducer (code link) :
const CREATE_NOTE = 'CREATE_NOTE';
const UPDATE_NOTE = 'UPDATE_NOTE';
const initialState = {
nextNoteId: 1.notes: {}};const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE: {
const id = state.nextNoteId;
const newNote = {
id,
content: ' '
};
return {
...state,
nextNoteId: id + 1.notes: {
...state.notes,
[id]: newNote
}
};
}
case UPDATE_NOTE: {
const {id, content} = action;
consteditedNote = { ... state.notes[id], content };return {
...state,
notes: {
...state.notes,
[id]: editedNote
}
};
}
default:
returnstate; }};const actions = [
{type: CREATE_NOTE},
{type: UPDATE_NOTE, id: 1.content: 'Hello, world! '}];const state = actions.reduce(reducer, undefined);
ReactDOM.render(
<pre>{JSON.stringify(state, null, 2)}</pre>.document.getElementById('root'));Copy the code
Now, you can see why Redux calls itself “a predictable JavaScript app state container.” Enter the same set of actions and you will get the same state. Functional programming wins! If you’ve ever heard that Redux can reproduce its previous state, this is how it works. In fact, instead of referring to an action list, Redux uses a variable to point to a state object, and then continually changes that variable to point to an object in the next state. This is an important mutation allowed in your application, but we need to keep it in store.
Store
So let’s create a store. It holds our individual state variables and provides some means of accessing them.
const validateAction = action= > {
if(! action ||typeofaction ! = ='object' || Array.isArray(action)) {
throw new Error('Action must be an object! ');
}
if (typeof action.type === 'undefined') {
throw new Error('Action must have a type! '); }};const createStore = (reducer) = > {
let state = undefined;
return {
dispatch: (action) = > {
validateAction(action)
state = reducer(state, action);
},
getState: () = > state
};
};
Copy the code
Now you can see why we used constants instead of strings. Our detection of actions is more relaxed than Redux’s, but it’s enough to make sure we don’t misspell the action type. If we pass in a string, the action will go directly to the reducer’s default branch (default of the switch) and nothing will happen and the error may be ignored. But if we use constants, the spelling error will cause undefined to be returned and an error thrown, so let’s find the error and fix it immediately.
Let’s create a store and use it:
// Pass in the reducer we made earlier.
const store = createStore(reducer);
store.dispatch({
type: CREATE_NOTE
});
store.getState();
/ / {
// nextNoteId: 2,
// notes: {
/ / 1: {
// id: 1,
// content: ''
/ /}
/ /}
// }
Copy the code
It’s available now. We have a store that can manage state using whatever reducer we provide. But one important piece is missing: a way to subscribe to state changes. Without this approach, we need some clunky imperative code. If we introduce asynchronous actions in the future, it won’t work at all. So let’s implement the subscription:
const createStore = reducer= > {
let state;
const subscribers = [];
const store = {
dispatch: action= > {
validateAction(action);
state = reducer(state, action);
subscribers.forEach(handler= > handler());
},
getState: () = > state,
subscribe: handler= > {
subscribers.push(handler);
return () = > {
const index = subscribers.indexOf(handler);
if (index > 0) {
subscribers.splice(index, 1); }}; }}; store.dispatch({type: '@@redux/INIT'});
return store;
};
Copy the code
This is a little extra code that’s not too hard to understand. The SUBSCRIBE function takes a handler function and adds it to the Subscribers list. It also returns a function to unsubscribe. Any time we call Dispatch, we notify all of these handlers. It is now easy to rerender every time the state changes.
///////////////////////////////
// Mini Redux implementation //
///////////////////////////////
const validateAction = action= > {
if(! action ||typeofaction ! = ='object' || Array.isArray(action)) {
throw new Error('Action must be an object! ');
}
if (typeof action.type === 'undefined') {
throw new Error('Action must have a type! '); }};const createStore = reducer= > {
let state;
const subscribers = [];
const store = {
dispatch: action= > {
validateAction(action);
state = reducer(state, action);
subscribers.forEach(handler= > handler());
},
getState: () = > state,
subscribe: handler= > {
subscribers.push(handler);
return () = > {
const index = subscribers.indexOf(handler);
if (index > 0) {
subscribers.splice(index, 1); }}; }}; store.dispatch({type: '@@redux/INIT'});
return store;
};
//////////////////////
// Our action types //
//////////////////////
const CREATE_NOTE = 'CREATE_NOTE';
const UPDATE_NOTE = 'UPDATE_NOTE';
/////////////////
// Our reducer //
/////////////////
const initialState = {
nextNoteId: 1.notes: {}};const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE: {
const id = state.nextNoteId;
const newNote = {
id,
content: ' '
};
return {
...state,
nextNoteId: id + 1.notes: {
...state.notes,
[id]: newNote
}
};
}
case UPDATE_NOTE: {
const {id, content} = action;
consteditedNote = { ... state.notes[id], content };return {
...state,
notes: {
...state.notes,
[id]: editedNote
}
};
}
default:
returnstate; }};///////////////
// Our store //
///////////////
const store = createStore(reducer);
///////////////////////////////////////////////
// Render our app whenever the store changes //
///////////////////////////////////////////////
store.subscribe(() = > {
ReactDOM.render(
<pre>{JSON.stringify(store.getState(), null, 2)}</pre>.document.getElementById('root')); });//////////////////////
// Dispatch actions //
//////////////////////
store.dispatch({
type: CREATE_NOTE
});
store.dispatch({
type: UPDATE_NOTE,
id: 1.content: 'Hello, world! '
});
Copy the code
You can try this code in JSFiddle and emit more actions. The rendered HTML will always reflect store state. Of course, for a real application, we would associate the dispatch function with the user’s action. We’ll get to that in a minute.
Create your own components
How do you write components that work with Redux? Simply accept the React component of the props. You implement your own states, so it’s ok to write components that work with at least some of those states. There are some special cases that can affect your component design (especially when it comes to performance issues), but for the most part, simple components will be fine. Let’s start with the simplest component:
const NoteEditor = ({note, onChangeNote, onCloseNote}) = > (
<div>
<div>
<textarea
className="editor-content"
autoFocus
value={note.content}
onChange={event= > onChangeNote(note.id, event.target.value)}
rows={10} cols={80}
/>
</div>
<button className="editor-button" onClick={onCloseNote}>Close</button>
</div>
);
const NoteTitle = ({note}) = > {
const title = note.content.split('\n') [0].replace(/^\s+|\s+$/g.' ');
if (title === ' ') {
return <i>Untitled</i>;
}
return <span>{title}</span>;
};
const NoteLink = ({note, onOpenNote}) = > (
<li className="note-list-item">
<a href="#" onClick={()= > onOpenNote(note.id)}>
<NoteTitle note={note}/>
</a>
</li>
);
const NoteList = ({notes, onOpenNote}) = > (
<ul className="note-list">
{
Object.keys(notes).map(id =>
<NoteLink
key={id}
note={notes[id]}
onOpenNote={onOpenNote}
/>)}</ul>
);
const NoteApp = ({ notes, openNoteId, onAddNote, onChangeNote, onOpenNote, onCloseNote }) = > (
<div>
{
openNoteId ?
<NoteEditor
note={notes[openNoteId]} onChangeNote={onChangeNote}
onCloseNote={onCloseNote}
/> :
<div>
<NoteList notes={notes} onOpenNote={onOpenNote}/>
<button className="editor-button" onClick={onAddNote}>New Note</button>
</div>
}
</div>
);
Copy the code
Nothing special. We can input props to these components and render them. But note the openNoteId attribute passed in and the callbacks to onOpenNote and onCloseNote: we need to decide where the status and callbacks are stored. We can use the component’s state directly, which is fine. But when you start using Redux, there is no rule that all states must be placed in the Redux Store. If you want to know when to use store store state, just ask yourself:
Does this state need to exist after the component is uninstalled?
If not, it is probably more appropriate to store state using the component’s own state. Redux is probably a better choice for state that needs to be kept on the server or shared across components that load and unload independently.
Sometimes Redux is good for volatile states. Especially if the state needs to change as the state in the store changes, it may be easier to store it in the store. For our application, when we create a note, we need to set openNoteId to the new note ID. To do this awkward in components, because we need to monitor the state of the store in componentWillReceiveProps change. I’m not saying it’s wrong, it’s just clumsy. So for our application, we’ll keep openNoteId in store state (in a real application, we might need routing as well. A brief introduction to routing is also provided below.
Another reason to put mutable state in the Store may be to make it easier to access from the Redux developer tools. The Redux development tool makes it much easier to view the data stored in the Store, as well as interesting features like state rollback. It is easy to start with the internal component state and switch to the Store state. Simply provide a container component to wrap local state, just as a store wraps global state.
So, let’s modify our Reducer to deal with mutable state:
const OPEN_NOTE = 'OPEN_NOTE';
const CLOSE_NOTE = 'CLOSE_NOTE';
const initialState = {
// ...
openNoteId: null
};
const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE: {
const id = state.nextNoteId;
// ...
return {
...state,
// ...
openNoteId: id,
// ...
};
}
// ...
case OPEN_NOTE: {
return {
...state,
openNoteId: action.id
};
}
case CLOSE_NOTE: {
return {
...state,
openNoteId: null
};
}
default:
returnstate; }};Copy the code
Hand assemble
Okay, now we can put the whole thing together. We do not modify existing components. We’ll create a new container component that gets the state from store and passes it to NoteApp:
class NoteAppContainer extends React.Component {
constructor(props) {
super(a);this.state = props.store.getState();
this.onAddNote = this.onAddNote.bind(this);
this.onChangeNote = this.onChangeNote.bind(this);
this.onOpenNote = this.onOpenNote.bind(this);
this.onCloseNote = this.onCloseNote.bind(this);
}
componentWillMount() {
this.unsubscribe = this.props.store.subscribe(() = >
this.setState(this.props.store.getState())
);
}
componentWillUnmount() {
this.unsubscribe();
}
onAddNote() {
this.props.store.dispatch({
type: CREATE_NOTE
});
}
onChangeNote(id, content) {
this.props.store.dispatch({
type: UPDATE_NOTE,
id,
content
});
}
onOpenNote(id) {
this.props.store.dispatch({
type: OPEN_NOTE,
id
});
}
onCloseNote() {
this.props.store.dispatch({
type: CLOSE_NOTE
});
}
render() {
return (
<NoteApp
{. this.state}
onAddNote={this.onAddNote}
onChangeNote={this.onChangeNote}
onOpenNote={this.onOpenNote}
onCloseNote={this.onCloseNote}
/>
);
}
}
ReactDOM.render(
<NoteAppContainer store={store}/>.document.getElementById('root'));Copy the code
Haha, that’s it! Try this app at JSFiddle
Now the application sends actions to make the reducer update the state data stored in the store, while the subscription ensures that the data rendered by the view is synchronized with the state data of the store. If we encounter a status data exception, we no longer need to check the component itself, but only the triggered actions and reducer.
The Provider and the Connect
All right, everything’s working. But… There are still some problems.
- The binding operation appears imperative.
- There is a lot of repetitive code in container components.
- You need to use global every time you bind a Store to a component
store
Object. Otherwise, we need to changestore
Spread throughout the component tree. Or we could bind once at the top node and putAll the thingsIt goes down the tree. This is not good in large applications.
So we need the Providers and connect provided in React Redux. First, create a Provider component:
class Provider extends React.Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: PropTypes.object
};
Copy the code
The code is simple. The Provider component uses the React Context feature to convert a store into a context property. Context is a way of passing information from the top component to the bottom component without requiring the intermediate component to explicitly pass information. In general, you should avoid using context, because the React documentation says:
If you want your application to be stable, don’t use context. This is an experimental API and may be abandoned in a future React release.
That’s why we use our own code implementation instead of using the context directly. We encapsulated the experimental API so that if it changed, we could change our implementation without the developer having to modify the code.
So we need a way to convert the context to props. That’s what CONNECT does.
const connect = (mapStateToProps = () => ({}), mapDispatchToProps = () => ({})) = > Component= > {
class Connected extends React.Component {
onStoreOrPropsChange(props) {
const {store} = this.context;
const state = store.getState();
const stateProps = mapStateToProps(state, props);
const dispatchProps = mapDispatchToProps(store.dispatch, props);
this.setState({ ... stateProps, ... dispatchProps }); }componentWillMount() {
const {store} = this.context;
this.onStoreOrPropsChange(this.props);
this.unsubscribe = store.subscribe(() = > this.onStoreOrPropsChange(this.props));
}
componentWillReceiveProps(nextProps) {
this.onStoreOrPropsChange(nextProps);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return <Component {. this.props} {. this.state} / >;
}
}
Connected.contextTypes = {
store: PropTypes.object
};
return Connected;
}
Copy the code
It’s a little bit complicated. To be honest, we’re a lot more lazy than the actual implementation (which we’ll discuss in the performance section at the end of this article), but we’re pretty close to the general principles of real Redux. Connect is a higher-order component, actually more like a higher-order function, that takes two functions and returns a function that takes the component as input and the new component as output. This component subscribes to the Store and updates your component props when changes occur. Start using Connect and it will become more practical.
Automatic assembly
const mapStateToProps = state= > ({
notes: state.notes,
openNoteId: state.openNoteId
});
const mapDispatchToProps = dispatch= > ({
onAddNote: () = > dispatch({
type: CREATE_NOTE
}),
onChangeNote: (id, content) = > dispatch({
type: UPDATE_NOTE,
id,
content
}),
onOpenNote: id= > dispatch({
type: OPEN_NOTE,
id
}),
onCloseNote: () = > dispatch({
type: CLOSE_NOTE
})
});
const NoteAppContainer = connect(
mapStateToProps,
mapDispatchToProps
)(NoteApp);
Copy the code
Hey, that looks better!
The first function passed to Connect (mapStateToProps) gets the current state from our store and returns some props. The second function passed to connect (mapDispatchToProps) gets our Store’s dispatch method and returns some props. Connect returns us a new function, and passing our component NoteApp to this function gives us a new component that will automatically get all of these props (and the extra parts we passed in).
Now we need to use our Provider component so that connect doesn’t have to put a store in the context.
ReactDOM.render(
<Provider store={store}>
<NoteAppContainer/>
</Provider>.document.getElementById('root'));Copy the code
Very good! Our store is passed in once at the top and then receives the store with Connect to do all the work (long live declarative programming!). . This is the application we put together with Provider and Connect
The middleware
Now we’ve written something useful, but there’s a piece missing: at some point, we need to talk to the server. Now that our actions are synchronous, how do we issue asynchronous actions? We can get the data in the component, but there are some problems with this:
- Redux (except
Provider
和connect
React is not specific. It is best to have a Redux solution. - When pulling data, we sometimes need to access state. We don’t want to transfer state all over the place. So we want to write something like
connect
Things to get data. - When we test for state changes involving data acquisition, we must test against components. We should try to decouple data acquisition.
- Some of the tools don’t work anymore.
Redux is synchronous, so what should we do? Put something between a dispatch and an operation that changes the state of a store. This is middleware.
First, we need a way to pass middleware to the store:
const createStore = (reducer, middleware) = > {
let state;
const subscribers = [];
const coreDispatch = action= > {
validateAction(action);
state = reducer(state, action);
subscribers.forEach(handler= > handler());
};
const getState = () = > state;
const store = {
dispatch: coreDispatch,
getState,
subscribe: handler= > {
subscribers.push(handler);
return () = > {
const index = subscribers.indexOf(handler)
if (index > 0) {
subscribers.splice(index, 1); }}; }};if (middleware) {
const dispatch = action= > store.dispatch(action);
store.dispatch = middleware({
dispatch,
getState
})(coreDispatch);
}
coreDispatch({type: '@@redux/INIT'});
return store;
}
Copy the code
It gets a little bit more complicated. What matters is the final if statement:
if (middleware) {
const dispatch = action= > store.dispatch(action);
store.dispatch = middleware({
dispatch,
getState
})(coreDispatch);
}
Copy the code
We create a function that “resends action” :
const dispatch = action= > store.dispatch(action);
Copy the code
If the middleware decides to issue a new action, the action will be passed down through the middleware. We need to create this function because we need to modify the Store dispatch method. (Another example of simplifying things with mutable objects, we can break rules when developing Redux, as long as it helps developers follow the rules. ^_^)
store.dispatch = middleware({
dispatch,
getState
})(coreDispatch);
Copy the code
The above code calls the middleware, passing it a re-dispatch function and a getState function. This middleware needs to return a new function with the ability to receive calls to the next Dispatch function (the original Dispatch function). If you’re feeling dizzy reading this, don’t worry. Creating and using middleware is actually quite easy.
Okay, let’s create a middleware with a one-second re-dispatch delay. It is of no practical use, but illustrates how asynchrony works:
const delayMiddleware = () = > next= > action= > {
setTimeout(() = > {
next(action);
}, 1000);
};
Copy the code
The signature of this function looks silly, but it fits into the jigsaw puzzle we created earlier. It is a function that returns a function that accepts the next dispatch function, which accepts the action. It may seem like Redux is going crazy with arrow functions, but there’s a reason for that, as we’ll explain shortly.
Now let’s start using this middleware in store.
const store = createStore(reducer, delayMiddleware);
Copy the code
Ha, we made our app slow down! That’s not good. But we have asynchronous operations! Please try this terrible app, the typing delay is ridiculous.
Adjusting setTimeout can make it worse, or better.
Assembled middleware
Let’s write a more useful middleware for logging:
const loggingMiddleware = ({getState}) = > next= > action= > {
console.info('before', getState());
console.info('action', action);
const result = next(action);
console.info('after', getState());
return result;
};
Copy the code
That’s useful. We added it to our store. But our Store can only accept one middleware function, so we need a way to assemble our middleware. So, we need a way to turn many middleware functions into one middleware function. Write applyMiddleware:
const applyMiddleware = (. middlewares) = > store= > {
if (middlewares.length === 0) {
return dispatch= > dispatch;
}
if (middlewares.length === 1) {
return middlewares[0](store);
}
const boundMiddlewares = middlewares.map(middleware= >
middleware(store)
);
return boundMiddlewares.reduce((a, b) = >
next= > a(b(next))
);
};
Copy the code
It’s not an elegant function, but you should be able to follow it. The first thing to notice is that it receives a list of middleware and returns a middleware function. This new middleware function has the same signature as the previous middleware. It takes a store (just the dispatch and getState methods for the new dispatch action, not the whole store) and returns another function. For this function:
- If we don’t have middleware, return the same function as before. Basically just a middleware that does nothing (stupid, but we just prevent people from doing damage).
- If we have a middleware, return the middleware function directly (also stupid, just a porter).
- We tied all our middleware to our fake store (finally fun).
- We bind these functions one by one to the next dispatch function. This is why our middleware has so many arrows. We have a function that takes action and keeps calling the next dispatch function until we reach the original Dispatch function.
Ok, now we can use all the middleware as expected:
const store = createStore(reducer, applyMiddleware(
delayMiddleware,
loggingMiddleware
));
Copy the code
Now our Redux implementation can do everything! Give it a try!
Open the console in a browser and you can see the logging middleware in action.
Thunk middleware
Let’s do something really asynchronous. Here is a “Thunk” middleware:
const thunkMiddleware = ({dispatch, getState}) = > next= > action= > {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
Copy the code
“Thunk” is really just another name for “function,” but it usually means “a function that encapsulates some work for future processing.” If we add thunkMiddleware:
const store = createStore(reducer, applyMiddleware(
thunkMiddleware,
loggingMiddleware
));
Copy the code
Now we can do this:
store.dispatch(({getState, dispatch}) = > {
// Fetch data from state
const someId = getState().someId;
// Pull data from the server based on the obtained data
fetchSomething(someId)
.then((something) = > {
// Action can be issued at any time
dispatch({
type: 'someAction',
something
});
});
});
Copy the code
Thunk middleware is a sledgehammer that can pull anything out of state and dispatch any action at any time. This is convenient and flexible, but it can become dangerous as your app gets bigger and bigger. But it works fine here. Let’s use it to do something asynchronous.
First, create a fake API:
const createFakeApi = () = > {
let _id = 0;
const createNote = () = > new Promise(resolve= > setTimeout(() = > {
_id++
resolve({
id: `${_id}`})},1000));
return {
createNote
};
};
const api = createFakeApi()
Copy the code
The API supports only one method that creates a note and returns the id of the note. Because we get the ID from the server, we need to make further changes to our Reducer:
const initialState = {
notes: {},
openNoteId: null.isLoading: false
};
const reducer = (state = initialState, action) = > {
switch (action.type) {
case CREATE_NOTE: {
if(! action.id) {return {
...state,
isLoading: true
};
}
const newNote = {
id: action.id,
content: ' '
};
return {
...state,
isLoading: false.openNoteId: action.id,
notes: {
...state.notes,
[action.id]: newNote
}
};
}
// ...}};Copy the code
Here, we are using CREATE_NOTE Action to set the loading state and create notes in the store. We mark this distinction only by the presence or absence of the ID attribute. You may need to use different actions, but Redux doesn’t care what you use. If you want some specifications, take a look at Flux Standard Actions.
Now, let’s modify mapDispatchToProps to issue thunk:
const mapDispatchToProps = dispatch= > ({
onAddNote: () = > dispatch(
(dispatch) = > {
dispatch({
type: CREATE_NOTE
});
api.createNote()
.then(({id}) = > {
dispatch({
type: CREATE_NOTE, id }); }); }),// ...
});
Copy the code
Our application is performing asynchronous operations! Give it a try!
But wait… In addition to adding ugly code to our components, we invented middleware to clean it up. But now it’s back in. We could have avoided this if we had created some custom API middleware instead of using Thunk. Even with thunk middleware, we can make our code more declarative.
The Action creator
We can abstract away the thunk action in a component and put it in a function:
const createNote = () = > {
return (dispatch) = > {
dispatch({
type: CREATE_NOTE
});
api.createNote()
.then(({id}) = > {
dispatch({
type: CREATE_NOTE, id }) }); }};Copy the code
The above code invents an Action creator. This isn’t anything fancy, just a function that returns action. It can:
- Abstract out ugly actions like our new Thunk action.
- This can help you implement the DRY principle if multiple components use the same action.
- Let’s make our component simpler by abstracting out the action that creates it.
We could have created the Action creator earlier, but there is no reason to do so. Our application is simple, so we don’t need to repeat the same action. Our actions are simple enough to be concise and declarative.
To modify our mapDispatchToProps, use the Action creator:
const mapDispatchToProps = dispatch= > ({
onAddNote: () = > dispatch(createNote()),
// ...
});
Copy the code
That’s better! This is our final application.
In this way!
You wrote a Redux yourself! This looks like a lot of code, but it’s mainly our Reducer and components. Our actual Redux implementation is less than 140 lines of code, including our Thunk and logging middleware, blank lines, and comments!
Real Redux and real applications are a little more complicated than that. We’ll discuss some of these unmentioned situations later, but hopefully this will give you some hope if you feel like you’ve fallen into the Redux trap.
Legacy matters
performance
What is missing from our implementation is the ability to listen to whether a particular property has actually changed. For our example application, this does not matter because each state change causes a property change. But for large applications with many mapStateToProps functions, we only want to update when the component actually receives new properties. It’s easy to extend our connect function to do this. We just need to compare the data before and after when we call setState. We need to get smarter with mapDispatchToProps. Notice that we are creating a new function each time. The real React Redux library checks the function’s arguments to see if it depends on properties. This way, if the property has not really changed, there is no need to do another mapping.
You also need to be aware that we call our function when we change the state of the property or store. These changes can happen simultaneously in an instant, wasting some performance. React Redux optimizes this, as well as many other things.
In addition, for larger applications, we need to consider the performance of the selector. For example, if we filter a list of notes, we don’t want to keep reevaluating the list. To do this, we need to use techniques such as ResELECT or other techniques to cache the results.
frozen
If you are using raw JS data structures (rather than something like immutable.js), one important detail I left out is freezing the reducer state at development time. Since this is JavaScript, there’s nothing to stop you from changing the state after you get it from the store. You can change it in the Render method or anything else. This can have very bad results and destroy some of the predictability that is being added through Redux. Here’s how I did it:
import deepFreeze from 'deep-freeze';
import reducer from 'your-reducer';
const frozenReducer = process.env.NODE_ENV === 'production' ? reducer : (
(. args) = > {
conststate = reducer(... args);returnfreezeState(state); });Copy the code
This creates a reducer that freezes the results. This way, if you want to change the store state in a component, it will report an error in the development environment. After a while, you’ll be able to avoid these mistakes. But if you’re new to immutable data, this is probably the easiest way to practice, both for you and your team.
Server side rendering
In addition to performance, we were lazy in our Connect implementation and neglected server-side rendering. ComponentWillMount can be called on the server, but we don’t want to set up a listener on the server. Redux uses componentDidMount and a few other tricks to make it work in the browser.
Store to enhance
We didn’t write a few higher-order functions, but here’s one missing: a “store enhancer” is a higher-order function that takes a store creator and returns an “enhanced” store creator. This is not a common operation, but it can be used to create things like Redux developer tools. A true applyMidleware implementation is a Store enhancer.
test
None of this Redux implementation has been tested. So whatever you do, don’t use this implementation in any actual production! This is only used in this article to illustrate the Redux principle!
The sorting
The note-taking app currently stores data in objects with numbers as keys. This means that each JS engine will sort them in the order they were created. If our server returns a GUID or some other unsorted primary key, we will have a hard time sorting. We don’t want to store notes in an array because it’s not easy to get specific notes by ID. So for real applications, we might want to store sorted ids in arrays. Alternatively, you can try using arrays if you use resELECT to cache the results of find operations.
Side effects of the Action creator
Sometimes, you might want to create middleware that looks like this:
store.dispatch(fetch('/something'));
Copy the code
Don’t do that; a function that returns a promise is actually running (unless it’s an abnormal delayed promise). This also means that we can’t handle the action with any middleware. For example, we can’t use throttling middleware. Also, we can’t use playback properly because we have to turn off the dispatch function. But any code that calls this Dispatch has already done its job, so it can’t be stopped.
Make sure your action is a description of the side effect, not the side effect itself. Thunk is opaque and not the best description, but they are also descriptions of side effects rather than side effects themselves.
routing
Routing can be strange, because the browser holds some state of the current location and some methods for changing the location. Once you start using Redux, you may want to put routing state in the Redux Store. That’s what I did, so I created a routing library to do just that. It’s cool to use newer versions of React routing as well, and there are other non-Redux routing solutions. Basically, you can find routing libraries to do as much work as you want.
Other items
There is an ecosystem of middleware and tools based on Redux. Here are some of the items you can check out, but familiarize yourself with the basics first!
You’ll definitely want to check out the Redux developer Tools extension or the code for the Redux Developer Tools themselves. These extensions are the easiest way to use developer tools.
Logger for Redux is a very convenient middleware that outputs actions to the console.
Redux-batched – Actions or redux-Batch can be useful if you want to issue multiple synchronized actions but only trigger a rerender once.
If asynchronous actions or side effects seem out of control with Redux-Thunk and you don’t want to write your own middleware, you can look at Redux-Saga or Redux-Logic. Or, if you want to dig deeper, redux-Loop is fun.
If you want to use GraphQL, look at Apollo, which can be combined with Redux.
Enjoy!
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.