Статьи

Государственное управление в React Native

Управление состоянием — одна из самых сложных концепций, которую нужно понять при изучении React Native, поскольку для этого существует множество способов. В реестре npm есть бесчисленное множество библиотек управления состояниями, таких как Redux, и существуют бесконечные библиотеки, построенные поверх других библиотек управления состояниями, чтобы упростить саму оригинальную библиотеку, например Redux Easy . Каждую неделю в React вводится новая библиотека управления состояниями, но базовые концепции поддержания состояния приложения остаются неизменными со времени введения React.

Наиболее распространенный способ установить состояние в React Native — использовать метод setState() React. У нас также есть API контекста, чтобы избежать детализации и пропустить состояние по многим уровням, не передавая его отдельным дочерним элементам в дереве.

Недавно в React появились версии 16.8.0 , которые представляют собой новый шаблон, упрощающий использование состояния в React. React Native получил его в v0.59 .

В этом уроке мы узнаем о том, что на самом деле является состоянием, а также о setState() , Context API и React Hooks. Это основа установки состояния в React Native. Все библиотеки сделаны на основе вышеуказанных базовых концепций. Поэтому, как только вы ознакомитесь с этими понятиями, вам будет легко понять библиотеку или создать собственную библиотеку управления состоянием.

Хотите узнать React Native с нуля? Эта статья является выдержкой из нашей Премиум библиотеки. Получите полную коллекцию книг React Native, охватывающую основы, проекты, советы и инструменты и многое другое с SitePoint Premium. Присоединяйтесь сейчас всего за $ 9 / месяц .

Что такое государство?

Все, что меняется со временем, называется состоянием . Если бы у нас было приложение Counter, состояние было бы самим счетчиком. Если бы у нас было приложение к списку дел, список задач со временем менялся бы, поэтому этот список был бы состоянием. Даже элемент ввода в некотором смысле является состоянием, так как со временем он вводится пользователем.

Введение в setState

Теперь, когда мы знаем, что такое состояние, давайте разберемся, как React хранит его.

Рассмотрим простое приложение-счетчик:

 import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => {}} title="Increment" /> <Button onPress={() => {}} title="Decrement" /> </> ) } } 

В этом приложении мы сохраняем наше состояние внутри constructor в объекте и присваиваем его this.state .

Помните, что состояние может быть только объектом. Вы не можете напрямую сохранить номер. Вот почему мы создали переменную counter внутри объекта.

В методе render мы деструктурируем свойство counter из this.state и this.state его внутри h1 . Обратите внимание, что в настоящее время будет отображаться только статическое значение ( 0 ).

Вы также можете написать свое состояние вне конструктора следующим образом:

 import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => {}} title="Increment" /> <Button onPress={() => {}} title="Decrement" /> </> ) } } 

Теперь давайте предположим, что мы хотим, чтобы кнопки + и - работали. Мы должны написать некоторый код внутри их соответствующих обработчиков onPress :

 import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => { this.setState({ counter: counter + 1 }) }} title="Increment" /> <Button onPress={() => { this.setState({ counter: counter - 1 }) }} title="Decrement" /> </> ) } } 

Теперь, когда мы нажимаем кнопки + и - , React выполняет рендеринг компонента. Это потому, что был использован метод setState() .

Метод setState() повторно отображает часть дерева, которая изменилась. В этом случае он повторно отображает h1 .

Поэтому, если мы нажимаем + , счетчик увеличивается на 1. Если мы нажимаем - , счетчик уменьшается на 1.

Помните, что вы не можете изменить состояние напрямую, изменив this.state ; делать this.state = counter + 1 не будет работать.

Кроме того, изменения состояния являются асинхронными операциями, что означает, что если вы читаете this.state сразу после вызова this.setState , это не будет отражать недавние изменения.

Здесь мы используем синтаксис «функция как обратный вызов» для setState() следующим образом:

 import React from 'react' import { Text, Button } from 'react-native' class Counter extends React.Component { state = { counter: 0 } render() { const { counter } = this.state return ( <> <Text>{counter}</Text> <Button onPress={() => { this.setState(prevState => ({ counter: prevState.counter + 1 })) }} title="Increment" /> <Button onPress={() => { this.setState(prevState => ({ counter: prevState.counter - 1 })) }} title="Decrement" /> </> ) } } 

Синтаксис «функция как обратный вызов» предоставляет недавнее состояние — в данном случае prevState — в качестве параметра для setState() .

Таким образом, мы получаем последние изменения в состоянии.

Какие крючки?

Крючки — это новое дополнение к React v16.8 . Ранее вы могли использовать только состояние, создав компонент класса. Вы не можете использовать состояние в самом функциональном компоненте.

С добавлением хуков вы можете использовать состояние в самом функциональном компоненте.

Давайте преобразуем наш вышеупомянутый компонент класса Counter функциональный компонент Counter и используем React Hooks:

 import React from 'react' import { Text, Button } from 'react-native' const Counter = () => { const [ counter, setCounter ] = React.useState(0) return ( <> <Text>{counter}</Text> <Button onPress={() => { setCounter(counter + 1 ) }} title="Increment" /> <Button onPress={() => { setCounter(counter - 1 ) }} title="Decrement" /> </> ) } 

Обратите внимание, что мы сократили наш компонент Class с 18 до 12 строк кода. Кроме того, код намного проще для чтения.

Давайте рассмотрим приведенный выше код. Во-первых, мы используем встроенный в useState метод useState . useState может быть любого типа — например, число, строка, массив, логическое значение, объект или любой тип данных — в отличие от setState() , который может иметь только объект.

В нашем примере счетчика он принимает число и возвращает массив с двумя значениями.

Первое значение в массиве является значением текущего состояния. Таким образом, counter равен 0 настоящее время.

Второе значение в массиве — это функция, которая позволяет обновлять значение состояния.

В нашем onPress мы можем обновить counter напрямую, используя setCounter .

Таким образом, наша функция приращения становится setCounter(counter + 1 ) а наша функция уменьшения становится setCounter(counter - 1) .

В React есть много встроенных хуков, таких как useState , useEffect , useContext , useReducer , useCallback , useMemo , useRef , useImperativeHandle , useLayoutEffect и useDebugValue которых вы можете узнать больше в документации по React Hooks .

Кроме того, мы можем создавать наши собственные крючки .

При построении или использовании хуков нужно соблюдать два правила:

  1. Только Call Hooks на верхнем уровне . Не вызывайте Hooks внутри циклов, условий или вложенных функций. Вместо этого всегда используйте Hooks на верхнем уровне вашей функции React. Следуя этому правилу, вы гарантируете, что хуки вызываются в одном и том же порядке каждый раз при рендеринге компонента. Это то, что позволяет React правильно сохранять состояние хуков между несколькими useState и useEffect .

  2. Вызывать только хуки из функций React . Не вызывайте хуки из обычных функций JavaScript. Вместо этого вы можете вызвать Hooks из функциональных компонентов React или вызвать Hooks из пользовательских Hooks.

Следуя этому правилу, вы гарантируете, что вся логика состояния в компоненте четко видна из его исходного кода.

Хуки действительно просты для понимания и полезны при добавлении состояния в функциональный компонент.

Контекст API

Контекст обеспечивает способ передачи данных через дерево компонентов без необходимости вручную пропускать подпорки на каждом уровне.

В типичном приложении React Native данные передаются через реквизиты сверху вниз. Если в приложении React имеется несколько уровней компонентов, и последний дочерний элемент в дереве компонентов хочет получить данные от самого верхнего родительского элемента, то вам придется передавать подпорки по отдельности.

Рассмотрим пример ниже. Мы хотим передать значение theme из компонента App компонент Pic . Как правило, без использования контекста, мы пройдем его через каждый промежуточный уровень следующим образом:

 const App = () => ( <> <Home theme="dark" /> <Settings /> </> ) const Home = () => ( <> <Profile /> <Timeline /> </> ) const Profile = () => ( <> <Pic theme={theme} /> <ChangePassword /> </> ) 

Ценность theme идет от App -> Home -> Profile -> Pic . Вышеуказанная проблема известна как пропеллерное бурение .

Это тривиальный пример, но рассмотрим реальное приложение, в котором есть десятки разных уровней.

Становится трудно передавать данные через каждого потомка, чтобы их можно было использовать в последнем потомке. Поэтому у нас есть Контекст.

Контекст позволяет нам напрямую передавать данные из App -> Pic .

Вот как это сделать, используя Context API:

 import React from 'react' const ThemeContext = React.createContext("light") // set light as default const App = () => ( <ThemeContext.Provider value="dark"> <Home /> <Settings /> </ThemeContext.Provider> ) const Home = () => ( <> <Profile /> <Timeline /> </> ) const Profile = () => ( <ThemeContext.Consumer> {theme => ( <Pic theme={theme} /> <ChangePassword /> )} </ThemeContext.Consumer> ) 

Во-первых, мы создаем ThemeContext с React.createContext API React.createContext . Мы устанавливаем light в качестве значения по умолчанию.

Затем мы оборачиваем корневой элемент нашего компонента App с помощью ThemeContext.Provider , предоставляя theme в качестве опоры.

Наконец, мы используем ThemeContext.Consumer в качестве рендеринга, чтобы получить значение theme dark .

Шаблон рендеринга реквизита хорош, но если у нас несколько контекстов, это может привести к ад-колбеку. Чтобы уберечь себя от ада обратного вызова, мы можем использовать Hooks вместо ThemeContext.Consumer .

Единственное, что нам нужно изменить, — это детали реализации компонента Profile :

 const Profile = () => { const theme = React.useContext(ThemeContext) return (<> <Pic theme={theme} /> <ChangePassword /> </> ) } 

Таким образом, нам не нужно беспокоиться об аде обратного вызова.

Совместное использование состояния между компонентами

До сих пор мы управляли только состоянием в самом компоненте. Теперь рассмотрим, как управлять состоянием компонентов.

Предположим, мы создаем простое приложение со списком дел следующим образом:

 import { View, Text } from 'react-native' const App = () => ( <> <AddTodo /> <TodoList /> </> ) const TodoList = ({ todos }) => ( <View> {todos.map(todo => ( <Text> {todo} </Text>) )} </View> ) 

Теперь, если мы хотим добавить AddTodo компонента AddTodo , как он будет отображаться в структуре TodoList компонента TodoList ? Ответ «поднятие состояния».

Если два родственных компонента хотят совместно использовать состояние, то состояние должно быть поднято до родительского компонента. Завершенный пример должен выглядеть так:

 import { View, Text, TextInput, Button } from 'react-native' const App = () => { const [ todos, setTodos ] = React.useState([]) return ( <> <AddTodo addTodo={todo => setTodos([...todos, todo])} /> <TodoList todos={todos} /> </> ) } const AddTodo = ({ addTodo }) => { const [ todo, setTodo ] = React.useState('') return ( <> <TextInput value={todo} onChangeText={value => setTodo(value)} /> <Button title="Add Todo" onPress={() => { addTodo(todo) setTodo('') }} /> </> ) } const TodoList = ({ todos }) => ( <View> {todos.map(todo => ( <Text> {todo} </Text>) )} </View> ) 

Здесь мы сохраняем состояние в компоненте App . Мы используем React Hook useState для хранения useState в виде пустого массива.

Затем мы addTodo метод AddTodo компоненту AddTodo а массив todos — компоненту TodoList .

Компонент AddTodo принимает метод addTodo в качестве опоры. Этот метод следует вызывать после button .

У нас также есть TextInput который также использует React Hook useState для отслеживания изменения значения TextInput .

После нажатия Button мы вызываем метод addTodo , который передается из родительского App . Это гарантирует, что todo будет добавлена ​​в список todos . И позже мы TextInput поле TextInput .

Компонент TodoList принимает TodoList и отображает список элементов todo , переданных ему.

Вы также можете попробовать удалить задачу, чтобы попрактиковаться в поднятии состояния самостоятельно. Вот решение:

 const App = () => { const [ todos, setTodos ] = React.useState([]) return ( <> <AddTodo addTodo={todo => setTodos([...todos, todo])} /> <TodoList todos={todos} deleteTodo={todo => setTodos(todos.filter(t => t !== todo))} /> </> ) } const TodoList = ({ todos, deleteTodo }) => ( <View> {todos.map(todo => ( <Text> {todo} <Button title="x" onPress={() => deleteTodo(todo)} /> </Text>) )} </View> ) 

Это самая распространенная практика в React. Поднять состояние не так просто, как кажется. Это простой пример, но в реальном приложении мы не знаем, какое состояние потребуется для поднятия его до родительского элемента, который будет использоваться в компоненте одного уровня. Поэтому сначала сохраняйте состояние в самом компоненте, а когда возникает ситуация, когда необходимо разделить состояние между компонентами, только поднимите состояние до родительского.

Таким образом, вы не делаете свой родительский компонент большим объектом состояния.

Вывод

Подводя итог, мы рассмотрели, что такое состояние и как установить значение состояния с помощью API setState() предоставляемого React. Мы также рассмотрели React Hooks, которые позволяют легко добавлять состояние к функциональному компоненту без необходимости преобразования его в компонент класса.

Мы узнали о новом Context API и его версии Hooks useContext , которая помогает нам избежать ада обратного вызова рендера.

Наконец, мы узнали о том, как поднять состояние, чтобы разделить состояние между родственными компонентами

Реакция становится очень простой, когда вы понимаете эти основные понятия. Не забывайте сохранять состояние как можно более локальным для компонента. Используйте API контекста только тогда, когда проблемное бурение становится проблемой. Поднимайте состояние только тогда, когда вам нужно.

Наконец, проверьте библиотеки управления состоянием, такие как Redux и MobX, как только ваше приложение станет сложным, и будет сложно отлаживать изменения состояния.