Hello, Habr.
Exactly a year has passed since I began to study React. During this time, I managed to release several small mobile applications written in React Native, and to participate in the development of a web application using ReactJS. Summing up and looking back at all those rakes that I managed to step on, I had the idea to express my experience in the form of an article. Iβll make a reservation that before starting the study of the reaction, I had 3 years of development experience in c ++, python, as well as the opinion that there is nothing complicated in front-end development and it will not be difficult to understand everything. Therefore, in the first months I neglected reading the educational literature and basically just google ready-made code examples. Accordingly, an exemplary developer who studies documentation first of all, most likely, will not find anything new for himself here, but I still think that quite a few people when studying new technology prefer the way from practice to theory. So if this article saves someone from a rake, then I tried not in vain.
Tip 1. Working with forms
Classic situation: there is a form with several fields into which the user enters data, then clicks on the button, and the entered data is sent to an external api / saved in state / displayed on the screen - underline the necessary.
Option 1. How not to do
In React, you can create a link to a DOM node or React component.
this.myRef = React.createRef();
Using the ref attribute, the created link can be attached to the required component / node.
<input id="data" type="text" ref={this.myRef} />
Thus, the problem above can be solved by creating a ref for each field of the form, and in the body of the function called when the button is clicked, get data from the form by contacting the necessary links.
class BadForm extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); this.onClickHandler = this.onClickHandler.bind(this); } onClickHandler() { const data = this.myRef.current.value; alert(data); } render() { return ( <> <form> <label htmlFor="data">Bad form:</label> <input id="data" type="text" ref={this.myRef} /> <input type="button" value="OK" onClick={this.onClickHandler} /> </form> </> ); } }
How the inner monkey can try to justify this decision:
- The main thing that works, you still have 100500 tasks, and
TV shows are not watched tickets are not closed. Leave it like that, then change - See how little code is needed to process the form. Declared ref and access data wherever you want.
- If you store the value in state, then every time you change the input data, the entire application will be rendered again, and you only need the final data. So this method also turns out to be good for optimization, just leave it that way.
Why the monkey is wrong:
The example above is the classic antipattern in React, which violates the concept of a unidirectional data stream. In this case, your application will not be able to respond to data changes during input, since they are not stored in state.
Option 2. Classic solution
For each form field, a variable is created in state, in which the input result will be stored. The value attribute is assigned this variable. The onhange attribute is assigned a function in which the value of the variable in state is changed through setState (). Thus, all data is taken from state, and when the data changes, state changes and the application is rendered again.
class GoodForm extends React.Component { constructor(props) { super(props); this.state = { data: '' }; this.onChangeData = this.onChangeData.bind(this); this.onClickHandler = this.onClickHandler.bind(this); } onChangeData(event) { this.setState({ data: event.target.value }); } onClickHandler(event) { const { data } = this.state; alert(data); } render() { const { data } = this.state; return ( <> <form> <label htmlFor="data">Good form:</label> <input id="data" type="text" value={data} onChange={this.onChangeData} /> <input type="button" value="OK" onClick={this.onClickHandler} /> </form> </> ); } }
Option 3. Advanced. When forms become many
The second option has a number of drawbacks: a large amount of standard code, for each field it is necessary to declare the onhange method and add a variable to state. When it comes to validating entered data and displaying error messages, the amount of code increases even more. To facilitate the work with forms, there is an excellent
Formik library that takes care of issues related to the maintenance of forms, and also makes it easy to add a validation scheme.
import React from 'react'; import { Formik } from 'formik'; import * as Yup from 'yup'; const SigninSchema = Yup.object().shape({ data: Yup.string() .min(2, 'Too Short!') .max(50, 'Too Long!') .required('Data required'), }); export default () => ( <div> <Formik initialValues={{ data: '' }} validationSchema={SigninSchema} onSubmit={(values) => { alert(values.data); }} render={(props) => ( <form onSubmit={props.handleSubmit}> <label>Formik form:</label> <input type="text" onChange={props.handleChange} onBlur={props.handleBlur} value={props.values.data} name="data" /> {props.errors.data && props.touched.data ? ( <div>{props.errors.data}</div> ) : null} <button type="submit">Ok</button> </form> )} /> </div> );
Tip 2. Avoid Mutation
Consider a simple to-do list application. In the constructor, we define in state the variable in which the to-do list will be stored. In the render () method, display the form through which we will add cases to the list. Now consider how we can change state.
Wrong option leading to array mutation:
this.state.data.push(item);
In this case, the array has really changed, but React knows nothing about it, which means the render () method will not be called, and our changes will not be displayed. The fact is that in JavaScript, when creating a new array or object, the link is saved in the variable, and not the object itself. Thus, adding a new element to the data array, we change the array itself, but not the link to it, which means that the data value stored in state will not change.
Mutations in JavaScript can be encountered at every turn. To avoid data mutations, use the spread operator or filter () and map () methods for arrays, and for objects use the spread operator or the assign () method.
const newData = [...data, item]; const copy = Object.assign({}, obj);
Returning to our application, it is worth saying that the correct option for changing state would be to use the setState () method. Do not try to change the state directly anywhere other than the constructor, as this contradicts the React ideology.
Do not do that!
this.state.data = [...data, item];
Also avoid the state mutation. Even if you use setState (), mutations can lead to bugs when trying to optimize. For example, if you pass a mutated object through props to a child PureComponent, then this component will not be able to understand that the received props have changed, and will not re-render.
Do not do that!
this.state.data.push(item); this.setState({ data: this.state.data });
The correct option:
const { data } = this.state; const newData = [...data, item]; this.setState({ data: newData });
But even the option above can lead to subtle bugs. The fact is that no one guarantees that during the time elapsed between receiving the data variable from state and writing its new value to state, the state itself will not change. Thus, you risk losing some of the changes made. Therefore, in the case when you need to update the value of a variable in state using its previous value, do it as follows:
The correct option, if the following condition depends on the current:
this.setState((state) => { return {data: [...state.data, item]}; });
Tip 3. Emulating a multi-page application
Your application is developing, and at some point you realize that you need multi-page. But what to do, because React is a single page application? At this point, the following crazy idea may come to your mind. You decide that you will keep the identifier of the current page in the global state of your application, for example, using the redux store. To display the desired page, you will use conditional rendering, and switch between pages, calling action with the desired payload, thereby changing the values ββin store redux.
App.js
import React from 'react'; import { connect } from 'react-redux'; import './App.css'; import Page1 from './Page1'; import Page2 from './Page2'; const mapStateToProps = (state) => ({ page: state.page }); function AppCon(props) { if (props.page === 'Page1') { return ( <div className="App"> <Page1 /> </div> ); } return ( <div className="App"> <Page2 /> </div> ); } const App = connect(mapStateToProps)(AppCon); export default App;
Page1.js
import React from 'react'; import { connect } from 'react-redux'; import { setPage } from './redux/actions'; function mapDispatchToProps(dispatch) { return { setPageHandle: (page) => dispatch(setPage(page)), }; } function Page1Con(props) { return ( <> <h3> Page 1 </h3> <input type="button" value="Go to page2" onClick={() => props.setPageHandle('Page2')} /> </> ); } const Page1 = connect(null, mapDispatchToProps)(Page1Con); export default Page1;
Why is this bad?
- This solution is an example of a primitive bicycle. If you know how to make such a bicycle competently and understand what you are going for, then it is not for me to advise you. Otherwise, your code will be implicit, confusing and unnecessarily complex.
- You will not be able to use the back button in the browser, as the history of visits will not be saved.
How to solve this?
Just use
react-router . This is a great package that can easily turn your application into a multi-page one.
Tip 4. Where to place api requests
At some point, you needed to add a request to an external api in your application. And here you are wondering: where in your application do you need to execute the request?
At the moment, when mounting a React component, its life cycle is as follows:
- constructor ()
- static getDerivedStateFromProps ()
- render ()
- componentDidMount ()
Let's analyze all the options in order.
In the constructor () method, the
documentation does not recommend doing anything other than:
- Initialization of the internal state through the assignment of the this.state object.
- Bindings of event handlers to an instance.
Calls to api are not included in this list, so let's move on.
The getDerivedStateFromProps () method according to the
documentation exists for rare situations when the state depends on changes in props. Again, not our case.
The most common mistake is the location of the code that executes api requests in the render () method. This leads to the fact that as soon as your request is successfully executed, you will most likely save the result in the state of the component, and this will lead to a new call to the render () method, in which your request to the api will be executed again. Thus, your component will end up in endless rendering, and this is clearly not what you need.
So the componentDidMount () method is the ideal place to access the external api.
Conclusion
Code examples can be found on
github .