Должен отметить что эта статья ни в коем случае не претендует на «best practice», и окажется для Вас полезной в том случае, если Вы, как и я в свое время, столкнулись с трудностью понимания принципов использования Redux в React.
P.S. В статье может быть целая куча пунктуационных ошибок, пусть они Вас не тревожат ;)
Я сам не являюсь frontend-разработчиком, но в своих побочных проектах я, как это часто бывает, и швец, и жнец, и на дуде игрец. Поэтому в той или иной степени пытаюсь и во фронт, хотя по большей степени предпочитаю пользоваться фреймворками, чтобы сэкономить время. Так например, я начал изучать React, и даже запилил mini-app «Вместе» для VK. Во время разработки я столкнулся с потребностью протаскивать состояние через длинную цепь компонентов, что иногда бывало затруднительно. И я задумался, а можно ли как-то менять состояние глобально?
Для этой цели как раз и существует Redux. Я не буду объяснять что это такое, если вы читаете это значит вы наверняка и сами уже сто раз прочли толковые статьи по типу этой. Тем не менее на всякий случай публикую наглядную схему:
Вот только примеры у всех этих статьей для меня оказались либо слишком сложными, либо слишком поверхностными. Я пытался сделать, казалось бы, простую вещь — по нажатию на кнопку на одном экране, глобально изменить текст на другом, который как раз берется из состояния приложения.
Говоря об экранах я имею ввиду React-Native , но в рамках статьи это не имеет никакого значения, принципы одни и те же. Вам не обязательно повторять за мной целиком, вы можете даже использовать совершенно другие компоненты и архитектуру приложения, главное обратите внимание на работу с Redux.
Итак, приступим. Предположим Вы уже создали проект реакт. Вот так выглядит мой основной компонент App:
import React from 'react'; // сам React
import {Provider} from 'react-redux'; // сам Redux
import { NavigationContainer } from '@react-navigation/native'; // React-Native
import { createDrawerNavigator } from '@react-navigation/drawer'; // Компонент навигации
import store from '../../store'; // объект хранилища, речь о создании которого будет ниже
/*Мои кастомные компоненты*/
import FirstScreen from '../firstscreen';
import SecondScreen from '../secondscreen';
const Drawer = createDrawerNavigator();
export default class App extends React.Component {
render(){
return(
<Provider store={store}>
<NavigationContainer>
<Drawer.Navigator initialRouteName="FirstScreen">
<Drawer.Screen name="FirstScreen" component={FirstScreen} />
<Drawer.Screen name="SecondScreen" component={SecondScreen} />
</Drawer.Navigator>
</NavigationContainer>
</Provider>
) ;
}
}
Мое приложение состоит из 2х экранов — FirstScreen и SecondScreen. На обоих экранах есть кнопки, которые могут менять текст друг у друга. Для этого и используется Redux. Создадим директорию store в корне приложения, а в ней следующие папки и файлы:
Что за что отвечает? Начнем с папки types. В ней хранятся пользовательские типы, передаваемые экшеном в редьюсером, на основе которых будут производится те или иные действия. То есть, это своеобразные флаги. Наверное это звучит сумбурно, но в дальнейшем будет понятней. Вот код единственного файла в папке:
/* store/types/index.js */
export const TEST_1 = "TEST_1";
export const TEST_2 = "TEST_2";
Все, как видите, ни для чего кроме как для хранения констант этот файл и не нужен. Идем дальше, в папку actions. Здесь хранятся действия, каждое из которых влияет на изменение глобального состояния приложения. Если Вы помните схему выше, то именно экшены дергают редьюсеры, которые уже непосредственно отражаются на состоянии.
/* store/actions/index.js */
import {TEST_1,TEST_2} from '../types';
export const change1stScreen = (text) => {
console.log("Action 1: "+ text);
return {
type: TEST_1,
payload: text
}
}
export const change2ndScreen = (text) => {
console.log("Action 2: "+ text);
return {
type: TEST_2,
payload: text
}
}
Что мы видим — у нас есть 2 действия, каждое из которых принимает на вход строку, и возвращает объект с 2мя свойствами: type и payload . Что такое type и так ясно а payload это и есть полезная информация, в нашем случае текст на который будет произведена замена при нажатии кнопки. Потому переходим в папку reducers, и смотрим что там.
1й редьюсер:
/* store/reducers/test1.js */
import {TEST_1} from '../types';
const INITIAL_STATE = {
text1: "default text 1"
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case TEST_1:
return {
...state, text1: action.payload // обновляем текст
}
default:
return state; // если тип не пришел, тогда просто возвращаем INITIAL_STATE
}
}
2й редьюсер:
/* store/reducers/test2.js */
import {TEST_2} from '../types';
const INITIAL_STATE = {
text2: "default text 2"
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case TEST_2:
return {
...state, text2: action.payload // обновляем текст
}
default:
return state; // если тип не пришел, тогда просто возвращаем INITIAL_STATE
}
}
В моем примере оба редьюсера почти идентичны, само собой я сделал это намеренно, чтобы было понятней. Что тут происходит — мы создаем дефолтное состояние (INITIAL_STATE), далее экспортируем лямбда функцию, которая в зависимости от типа обновляет состояние на пришедший payload. Как видите тип позволяет сделать процесс работы с редьюсером более гибким, и например обновлять несколько свойств состояния за раз (в моем примере это не рассматривается, но это не должно вызывать какие либо сложности). По дефолту мы возвращаем INITIAL_STATE.
В индексном файле мы объединяем все наши редьюсеры в 1. Это удобнее чем создавать один огромный редьюсер.
/* store/reducers/index.js */
import {combineReducers} from 'redux';
import test1 from './test1';
import test2 from './test2';
export default combineReducers({
testReducer1: test1,
testReducer2: test2
});
Ну и наконец создаем хранилище в файле index.js папки store:
/* store/index.js */
import {createStore} from 'redux';
import reducers from './reducers';
const store = createStore(reducers);
export default store;
Дело осталось за малым, применить все вышеописанное в наших экранах FirstScreen и SecondScreen
// firstscreen.js
import React from 'react';
import {
View,
Text,
Button
} from 'react-native';
import { connect } from 'react-redux';
import { change2ndScreen } from '../../store/actions';
import { bindActionCreators } from 'redux';
class FirstScreen extends React.Component {
constructor(props){
super(props);
}
render(){
let {text1} = this.props;
return(
<View>
<Text>Text 1st Screen: {text1}</Text>
<Button onPress={() => this.props. change2ndScreen("new text 2")} title="change 2nd Screen Text"></Button>
</View>
);
}
}
const mapStateToProps = state => ({
text1: state.testReducer1.text1
});
const mapDispatchToProps = dispatch => ({
change2ndScreen: bindActionCreators(change2ndScreen, dispatch)
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(FirstScreen);
Объясняю по порядку — компонент FirstScreen выступает в роли первого экрана. Он содержит текст со строкой содержащей строку из сформированного нами ранее хранилища (store) и кнопку, изменяющую текст второго экрана с помощью экшена change2ndScreen. Свойство text1 было у первого редьюсера. А получаем мы всю эту канитель с помощью mapStateToProps . Точно также мы получаем и необходимые нам экшены, но уже через mapDispatchToProps (их мы вынуждены импортировать вручную, прежде чем привязать). Далее мы привязываем через connect() полученные от редьюсера свойства и экшены к объекту props а не в state компонента. Чаще всего когда говорят о Redux фигурирует слово «состояние», что может вызвать путаницу.
Если у Вас возник вопрос а как redux получает доступ к store в этом компоненте, то обратите внимание на Provider в примере кода главного компонента App. В Provider обернуто все приложение. Это и обеспечивает связь низкоуровневых компонентов с хранилищем через Redux.
По той же схеме сделан и компонент SecondScreen
// secondscreen.js
import React from 'react';
import {
View,
Text,
Button
} from 'react-native';
import { connect } from 'react-redux';
import { change1stScreen } from '../../store/actions';
import { bindActionCreators } from 'redux';
class SecondScreen extends React.Component {
constructor(props){
super(props);
}
render(){
let {text2} = this.props;
return(
<View>
<Text>Text 2nd Screen: {text2}</Text>
<Button onPress={() => this.props.change1stScreen("new text 1")} title="change 1st Screen Text"></Button>
</View>
);
}
}
const mapStateToProps = state => ({
text2: state.testReducer2.text2
});
const mapDispatchToProps = dispatch => ({
change1stScreen : bindActionCreators(change1stScreen, dispatch)
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(SecondScreen);
Вот и все. Надеюсь что у Вас получилось вникнуть в то как использовать Redux в React на моем ( худо-бедно, но все же) примере. Попробуйте воспроизвести и убедиться в его работоспособности. Или поупражняйтесь чтобы закрепить полученные знания.