1. The introduction
While React state management is a perennial issue, there is also a fair amount of information available online and in the community. I still want to summarize my impressions on state management from the time I got to know React. All new technologies emerge and are popular to solve scenario-specific problems, and we’ll start our story with a very simple example. There is a requirement that we need to display information about a product on the interface. Maybe we can implement it like this:
import React, { PureComponent } from 'react';
export default class ProductInfo extends PureComponent {
constructor(props) {
super(props);
this.state = {
data: {
sku: ' ',
desc: ' ',}}; }componentDidMount() {
fetch('url', { id: this.props.id })
.then(resp => resp.json())
.then(data => this.setState({ data }));
}
render() {
const { sku } = this.state.data;
return( {sku} ); }}Copy the code
Although the above scenario is very simple, but in our actual requirements development is very common, using the above way can also be a good solution to this kind of problem. Let’s make the scene a little more complicated. Suppose there are two parts of the interface that need to display the information of the goods, but the attributes of the goods displayed are different. We could write a similar component like the one above, but the problem is that we get the same product information twice. To avoid getting the data twice, we need to share the product information between the two components.
2. Props solves data sharing
Addressing the data sharing problem with props essentially puts the logic for retrieving data into the common parent of the component. The code might look like this:
import React, { PureComponent } from 'react';
export default class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
data: {
sku: ' ',
desc: ' ',}}; }componentDidMount() {
fetch('url', { id: this.props.id })
.then(resp => resp.json())
.then(data => this.setState({ data }));
}
render() {
return(a); }}function ProductInfoOne({ data }) {
const { sku } = data;
return {sku}
;
}
function ProductInfoTwo({ data }) {
const { desc } = data;
return {desc}
;
}
Copy the code
Scenarios where there are only 1 or 2 levels of component nesting can be best solved by moving the logic of data retrieval and storage up to the common parent. But if the interface rendering is a little more complex, such as ProductInfoOne’s child components that also need to render product information, we might want to continue passing data down through props. The problem is that as the nesting level gets deeper, the data needs to be passed from the outermost layer to the innermost layer. The overall code becomes less readable and maintainable. We want to break down the “pass-through” of data so that the child component can also fetch data from the parent component.
3. Context API
React 16.3 introduced the new Context API, which itself is designed to solve the problem of data transfer in deeply nested scenarios, and looks like a good fit for the problem we mentioned above. We tried to use the Context API to solve our problem
// context.js
const ProductContext = React.createContext({
sku: ' ',
desc: ' '});export default ProductContext;
// App.js
import React, { PureComponent } from 'react';
import ProductContext from './context';
const Provider = ProductContext.Provider;
export default class App extends PureComponent {
constructor(props) {
super(props);
this.state = {
data: {
sku: ' ',
desc: ' ',}}; }componentDidMount() {
fetch('url', { id: this.props.id })
.then(resp => resp.json())
.then(data => this.setState({ data }));
}
render() {
return (
);
}
}
// ProductInfoOne.js
import React, { PureComponent } from 'react';
import ProductContext from './context';
export default class ProductInfoOne extends PureComponent {
static contextType = ProductContext;
render() {
const { sku } = this.context;
return {sku}
;
}
}
// ProductInfoTwo.js
import React, { PureComponent } from 'react';
import ProductContext from './context';
export default class ProductInfoTwo extends PureComponent {
static contextType = ProductContext;
render() {
const { desc } = this.context;
return{desc} ; }}Copy the code
So far, we have only used the React library itself without introducing any third-party libraries. In fact, the above method is the most direct and simple solution for such simple scenarios. In the above scenarios, we lay emphasis on the presentation of information, while in the real scenario, we cannot avoid some interactive operations. For example, we need to present the product information and edit the product information at the same time. Since ProductInfoOne and ProductInfoTwo are controlled components and the data source is in the App component, we may pass the “callback function” for modifying data through the Context API to implement the data modification. In the above scenarios, we focus on the sharing of data between components with nested relationships. If the scenario becomes more complex and parallel components need to share data, for example, App1, which has no parent-child relationship with App, also needs to present product information, what do we do? It seems that Conext API is helpless.
4. Redux
Finally in the story, believes that many readers feel having repetitive, but based on the principle of technical solution is to solve a particular problem, or feel the need to do some foreshadowing, if you don’t complicated the problem scenario to React itself is not very good solution, advice and don’t introduce additional technology (except for have a better solution), Including the story. Redux is certainly powerful and is currently the most active and widely used solution in React state management. Here’s a quick illustration of Redux’s approach to the problem:
// store.js
import { createStore } from 'redux';
import reducer from './reducer';
const store = createStore(reducer);
export default store;
// reducer.js
import * as actions from './actions';
import { combineReducers } from 'redux';
function ProductInfo(state = {}, action) {
switch (action.type) {
case actions.SET_SKU: {
return { ...state, sku: action.sku };
}
case actions.SET_DESC: {
return { ...state, desc: action.desc };
}
case actions.SET_DATA: {
return{... state, ... action.data }; } default: {return state;
}
}
}
const reducer = combineReducers({
ProductInfo,
});
export default reducer;
// action.js
export const SET_SKU = 'SET_SKU';
export const SET_DESC = 'SET_DESC';
export const SET_DATA = 'SET_DATA';
export function setSku(sku) {
return {
type: SET_SKU,
sku,
};
}
export function setDesc(desc) {
return {
type: SET_DESC,
desc,
};
}
export function setData(data) {
return {
type: SET_DESC,
data,
};
}
// App.js
import React, { PureComponent } from 'react';
import { Provider } from 'react-redux';
import store from './store';
import * as actions from './actions';
class App extends PureComponent {
componentDidMount() {
fetch('url', { id: this.props.id })
.then(resp => resp.json())
.then(data => this.props.dispatch(actions.setData(data)));
}
render() {
return(a); }}function mapStateToProps() {
return{}; }function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
// ProductInfoOne.js
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';
class ProductInfoOne extends PureComponent {
onEditSku = (sku) => {
this.props.dispatch(actions.setSku(sku));
};
render() {
const { sku } = this.props.data;
return( {sku} ); }}function mapStateToProps(state) {
return {
data: state.ProductInfo,
};
}
function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductInfoOne);
// ProductInfoTwo.js
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import * as actions from './actions';
class ProductInfoTwo extends PureComponent {
onEditDesc = (desc) => {
this.props.dispatch(actions.setDesc(desc));
};
render() {
const { desc } = this.props.data;
return( {desc} ); }}function mapStateToProps(state) {
return {
data: state.ProductInfo,
};
}
function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductInfoTwo);
Copy the code
Redux can indeed solve the problems mentioned above. From the code and principle of Redux, we can know that Redux has done a lot of abstraction and stratification of concepts. Store is responsible for data storage, action is used to describe data modification actions, reducer is used to modify data. At first glance, Redux makes our code more complex, but it abstracts concepts and enforces rules that make it easier to share and modify data, conventions that make code more logical and maintainable in large projects with multiple partners. However, Redux has its fair share of critics, including a growing number of online critics. Aside from the cost of learning the technology, there are a few things I find maddening about using it:
- For the “simple” system is too wordy, the system the author is in charge of is biased to the background system, the system itself is not complex, and is a person responsible for development, in order to modify a certain data, need to modify multiple files; To look at the logic of a data change after a period of time, we need to go through the whole process of data change, which is not direct enough. Especially when you need to handle asynchronous operations, you need to introduce side-effect handling libraries such as Redux-Thunk, Redux-Saga, and Redux-Observables, which can make a simple system even more complex.
- Data caching issues. In Redux, store is a globally unique object and does not die when a component dies. The argument is that Redux naturally supports scenarios where data needs to be cached; However, in some scenarios that do not need caching, it may bring very serious consequences. For example, the author is responsible for the development of a commodity transaction page. Every time I jump to this page, the information of the commodity will be obtained and stored in the store. As a result, part of the product information stored in store is the cached product information of the last purchase, which will lead to the wrong product information presented on the interface. For this scenario, we also need an additional piece of code to handle the data cached in the store, either clearing the corresponding cache when the component is destroyed, or processing the cache in the store before fetching data or in a function that fails to fetch data. Are there any more lightweight state management libraries?
5. MobX
Mobx started to release its first version in 2016. In just over two years, it has developed rapidly and attracted more and more people’s attention. The realization idea of MobX is very simple and direct, similar to the principle of responsivity in Vue. Its essence can be simply understood as the observer mode, in which the data is the observed object and the “response” is the observer. The response can be calculated value or function. To illustrate the principle, use a picture from the Internet:
// store.js
import { observable } from 'mobx';
const store = observable({
sku: ' ',
desc: ' '});export default store;
// App.js
import React, { PureComponent } from 'react';
import store from './store.js';
export default class App extends PureComponent {
componentDidMount() {
fetch('url', { id: this.props.id })
.then(resp => resp.json())
.then(data => Object.assign(store, data));
}
render() {
return (
);
}
}
// ProductInfoOne.js
import React, { PureComponent } from 'react';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import store from './store';
@observer
class ProductInfoOne extends PureComponent {
@action
onEditSku = (sku) => {
store.sku = sku;
};
render() {
const { sku } = store;
return( {sku} ); }}export default ProductInfoOne;
// ProductInfoTwo.js
import React, { PureComponent } from 'react';
import { action } from 'mobx';
import { observer } from 'mobx-react';
import store from './store';
@observer
class ProductInfoTwo extends PureComponent {
@action
onEditDesc = (desc) => {
store.desc = desc;
};
render() {
const { desc } = store;
return( {desc} ); }}export default ProductInfoTwo;
Copy the code
To explain a little bit about the new terms used, observable or @Observable denotes an observable, and @observer denotes an observer. Essentially, the render method in the component is wrapped in autorun. @action indicates that this is an action to modify data. This annotation is optional, but it is recommended that it be used because the code logic is clearer, the underlying performance is optimized, and debugging tools can provide useful information when debugging.
In contrast to Redux, the code is much less with MobX, and the logic of data flow and modification is much more straightforward and clear. Declare an observable object, change the render function in the component to an observer using @Observer, and modify the data directly to modify the object’s properties. That’s all we need to do.
As you can see, Mobx’s data modification is at best “flexible” and at worst “arbitrary.” Fortunately, there are other libraries in the community to optimize this problem. For example, mobx-state-tree defines actions at the time of model definition, and manages data modification actions in one place. Mobx is much more flexible than Redux. It doesn’t have many constraints and rules. It can be very free and efficient with a small number of developers or small projects, but as the complexity of the project and the number of developers increases, this “no-constraint” can lead to high maintenance costs. Redux’s “constraints”, on the other hand, ensure that code written by different people is almost the same, because you have to follow its rules, and the code is more consistent and maintainable.
6. GraphQL
Both Redux and MobX focus on managing data, more specifically how it is stored and updated, but they don’t care where it comes from. So is there a new way to manage data in the future? GraphQL actually offers a new way to manage data.
When we develop a component or front-end system, part of the data comes from the background, such as the commodity information in the above scene, and part of the data comes from the foreground, such as whether the dialog box pops up. GraphQL unifies remote data with local data, allowing developers to feel that all data is queried. Developers do not need to care whether it is queried from the server or from local data. The principle and usage of GraphQL will not be explained here. If you are interested, please refer to the official website. Let’s see what the code would look like if GraphQL were used to solve the above problem.
// client.js
import ApolloClient from 'apollo-boost';
const client = new ApolloClient({
uri: 'http://localhost:3011/graphql/productinfo'
});
export default client;
// app.js
import React from 'react';
import { ApolloProvider, Query, Mutation } from 'react-apollo';
import gql from 'graphql-tag';
import client from './index';
import ProductInfoOne from './ProductInfoOne';
import ProductInfoTwo from './ProductInfoTwo';
const GET_PRODUCT_INFO = gql`
query ProductInfo($id: Int) {
productInfo(id: $id){
id
sku
desc
}
}
`;
export default class App extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
id: 1,
};
}
render() {
return (
{({ loading, error, data }) => {
if (loading) return 'loading... ';
if (error) return 'error... ';
if (data) {
return (
);
}
returnnull; }}); } } // ProductInfoOne.js import React from'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';
const SET_SKU = gql`
mutation SetSku($id: Int, $sku: String){
setSku(id: $id, sku: $sku) {
id
sku
desc
}
}
`;
export default class ProductInfoOne extends React.PureComponent {
render() {
const { id, sku } = this.props.data;
return (
{sku}
{(setSku) => (
{ setSku({ variables: { id: id, sku: 'new sku'}})}}> Modify sku)}); } } // ProductInfoTwo.js import React from'react';
import { Mutation } from 'react-apollo';
import gql from 'graphql-tag';
const SET_DESC = gql`
mutation SetDesc($id: Int, $desc: String){
setDesc(id: $id, desc: $desc) {
id
sku
desc
}
}
`;
export default class ProductInfoTwo extends React.PureComponent {
render() {
const { id, desc } = this.props.data;
return (
{desc}
{(setDesc) => (
{ setDesc({ variables: { id: id, desc: 'new desc' } }) }}>修改 desc
)}
);
}
}
Copy the code
As you can see, GraphQL encapsulates data as a Query GraphQL statement, and updates data as a Mutation GraphQL statement. For developers, I need data, so I need a Query, and I need to update data. So I need a Mutation action, and the data can come from either a remote server or a local one.
The biggest problem with GraphQL is that it requires a server-side interface to support GraphQL in order to truly play its power. Although several mainstream Web server-side languages, such as Java, PHP, Python and JavaScript, all have corresponding implementation versions. However, it will cost a lot to modify the existing system to support GraphQL. And GraphQL is not cheap to learn.
But GraphQL does offer a new way to think about traditional state management schemes. When we make interfaces with background personnel, there are always some vague and controversial gray areas. For example, when we want to display a list on the page, the front-end programmer thinks that a row in the table is a whole, and the background should return an array, and each element in the array corresponds to a row in the table. However, while back-end programmers may differentiate between dynamic and static data in data model design, the foreground should take dynamic and static data separately and assemble them into a row. The backend programmer’s mindset is what I have, is the producer’s perspective; The front-end programmer’s mindset is what I need, the consumer’s perspective. However, GraphQL forces the backend developer to develop the interface from the consumer’s point of view, because query parameters in GraphQL are often deduced from the interface rendering. In this way, the front end will reduce part of the dispute with the background interface, but also part of the work “transferred” to the background.
7. To summarize
- It is recommended to solve problems from 1, 2, and 3 points first.
- In small projects or projects with a small number of developers, MobX can be more efficient.
- Consider Redux for large projects or multi-person projects, with lower maintenance costs.
- GraphQL focuses on learning and understanding its ideas, which you can try to use in your personal projects.
www.yuque.com/es2049/blog…