Простейшее приложение
Стилизация компонентов
Использование стилей
Задание абсолютных размеров элементов
Задание размеров через flex
Задание относительных размеров
Атрибут flexDirection
Атрибут direction
Атрибут justifyContent
Атрибут alignSelf
Атрибут alignItems
Атрибут alignContent
Атрибут flexWrap
Интерактивные элементы
Нативные компоненты
Основные кнопки
Элементы Touchables
Анимация
Хуки
Хук состояния
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>Вы кликнули {this.state.count} раз(а)</p>
<button onClick={() => this.setState({
count: this.state.count + 1
})}>
Нажми на меня
</button>
</div>
);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import React, { useState } from 'react';
function Example() {
// Объявляем новую переменную состояния "count"
const [count, setCount] = useState(0);
return (
<div>
<p>Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>
Нажми на меня
</button>
</div>
);
}
|
1
2
3
4
5
6
7
| import React, { useState } from 'react';
function ExampleWithManyStates() {
// Объявим несколько переменных состояния!
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('банан');
const [todos, setTodos] = useState([{ text: 'Изучить хуки' }]);
|
Хук эффекта
Иногда мы хотим выполнить дополнительный код после того, как React обновил DOM. Сетевые запросы, изменения DOM вручную, логирование — всё это примеры эффектов, которые не требуют сброса. После того, как мы запустили их, можно сразу забыть о них, ведь больше никаких дополнительных действий не требуется. Давайте сравним, как классы и хуки позволяют нам реализовывать побочные эффекты.
В классовых React-компонентах метод render сам по себе не должен вызывать никаких побочных эффектов. Он не подходит для этих целей, так как в основном мы хотим выполнить наши эффекты после того, как React обновил DOM.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `Вы нажали ${this.state.count} раз`;
}
componentDidUpdate() {
document.title = `Вы нажали ${this.state.count} раз`;
}
render() {
return (
<div>
<p>Вы нажали {this.state.count} раз</p>
<button onClick={() => this.setState({
count: this.state.count + 1 })}>
Нажми на меня
</button>
</div>
);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `Вы нажали ${count} раз`;
});
return (
<div>
<p>Вы нажали {count} раз</p>
<button onClick={() => setCount(count + 1)}>
Нажми на меня
</button>
</div>
);
}
|
Эффекты со сбросом
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| class FriendStatus extends React.Component {
constructor(props) {
super(props);
this.state = { isOnline: null };
this.handleStatusChange = this.handleStatusChange.bind(this);
}
componentDidMount() {
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() {
ChatAPI.unsubscribeFromFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
handleStatusChange(status) {
this.setState({
isOnline: status.isOnline
});
}
render() {
if (this.state.isOnline === null) {
return 'Загрузка...';
}
return this.state.isOnline ? 'В сети' : 'Не в сети';
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import React, { useState, useEffect } from 'react';
function FriendStatus(props) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
// Указываем, как сбросить этот эффект:
return function cleanup() {
ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
};
});
if (isOnline === null) {
return 'Загрузка...';
}
return isOnline ? 'В сети' : 'Не в сети';
}
|
Навигация
StackNavigator
Библиотека React Navigation помогает организовать приложение, состоящее из нескольких экранов с возможностью передачи информации между этими экранами.
В версии 6.х интерфейс этой библиотеки сильно изменился. Подробнее в документации.
1
2
3
4
5
6
7
8
9
10
11
| npx react-native init MySocialNetwork --version 0.63.2
cd MySocialNetwork
npm run android
cp App.js HomeScreen.js
cp App.js FriendsScreen.js
npm install @react-navigation/native@5.7.3
npm install @react-navigation/stack@5.9.0 \
@react-native-community/masked-view@0.1.10 \
react-native-screens@2.10.1 \
react-native-safe-area-context@3.1.4 \
react-native-gesture-handler@1.7.0
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Welcome to MySocialNetwork!</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
export default App;
|
Добавим два новых экрана
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class HomeScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>You have (undefined) friends.</Text>
</View>
);
}
}
// ...
export default HomeScreen;
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
class FriendsScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Add friends here!</Text>
</View>
);
}
}
// ...
export default FriendsScreen;
|
Добавим навигацию:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // App.js
import 'react-native-gesture-handler';
import React from 'react';
import { StyleSheet } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from './HomeScreen';
import FriendsScreen from './FriendsScreen';
const Stack = createStackNavigator();
class App extends React.Component {
render() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
/>
<Stack.Screen
name="Friends"
component={FriendsScreen}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
}
// ...
|
Добавим кнопки перехода:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| class HomeScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>You have (undefined) friends.</Text>
<Button
title="Add some friends"
onPress={() =>
this.props.navigation.navigate('Friends')
}
/>
</View>
);
}
}
|
Использование контекста
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // App.js
// ...
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
possibleFriends: [
'Alice',
'Bob',
'Sammy',
],
currentFriends: [],
}
}
// ...
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // App.js
// ...
class App extends React.Component {
// ...
addFriend = (index) => {
const {
currentFriends,
possibleFriends,
} = this.state
// Pull friend out of possibleFriends
const addedFriend = possibleFriends.splice(index, 1)
// And put friend in currentFriends
currentFriends.push(addedFriend)
// Finally, update the app state
this.setState({
currentFriends,
possibleFriends,
})
}
// ...
|
1
2
3
4
5
| // FriendsContext.js
import React from 'react';
export const FriendsContext = React.createContext();
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // App.js
// ...
import { FriendsContext } from './FriendsContext';
class App extends React.Component {
// ...
render() {
return (
<FriendsContext.Provider
value={
{
currentFriends: this.state.currentFriends,
possibleFriends: this.state.possibleFriends,
addFriend: this.addFriend
}
}
>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={Home}
/>
<Stack.Screen
name="Friends"
component={Friends}
/>
</Stack.Navigator>
</NavigationContainer>
</FriendsContext.Provider>
);
}
}
// ...
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| // HomeScreen.js
import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
import { FriendsContext } from './FriendsContext';
class Home extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>You have { this.context.currentFriends.length } friends!</Text>
<Button
title="Add some friends"
onPress={() =>
this.props.navigation.navigate('Friends')
}
/>
</View>
);
}
}
HomeScreen.contextType = FriendsContext;
// ...
|
1
2
3
4
5
6
7
8
9
10
11
12
| // FriendsScreen.js
import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';
import { FriendsContext } from './FriendsContext';
class FriendsScreen extends React.Component {
// ...
}
FriendsScreen.contextType = FriendsContext;
// ...
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // FriendsScreen.js
// ...
class Friends extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Add friends here!</Text>
{
this.context.possibleFriends.map((friend, index) => (
<Button
key={ friend }
title={ `Add ${ friend }` }
onPress={() =>
this.context.addFriend(index)
}
/>
))
}
// ...
|
Элемент TabNavigation
Элемент DrawerNavigation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| import * as React from 'react';
import { Button, View } from 'react-native';
import { createDrawerNavigator } from '@react-navigation/drawer';
import { NavigationContainer } from '@react-navigation/native';
function HomeScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button
onPress={() => navigation.navigate('Notifications')}
title="Go to notifications"
/>
</View>
);
}
function NotificationsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button onPress={() => navigation.goBack()} title="Go back home" />
</View>
);
}
const Drawer = createDrawerNavigator();
export default function App() {
return (
<NavigationContainer>
<Drawer.Navigator initialRouteName="Home">
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Notifications" component={NotificationsScreen} />
</Drawer.Navigator>
</NavigationContainer>
);
}
|
Возврат к предыдущему экрану
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import * as React from 'react';
import { Button, View, Text } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details')}
/>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
<Button
title="Go back to first screen in stack"
onPress={() => navigation.popToTop()}
/>
</View>
);
}
|
Использование Redux
Основные понятия
Redux это контейнер состояний для приложений на React и полезный инструмент для управления состоянием приложений. Он предназначен для использования совместно с React.js, но его можно использовать и отдельно и с другими фронтенд фреймворками. Redux целесообразно использовать в больших и сложных приложениях, чем приложение сложнее, тем больше оно выиграет от использования redux.
Редьюсеры - это чистые функции, которые принимают предыдущее состояние и действие. Они возвращают новое состояние. Действие - это объект с типом и необязательными данными. Редьюсеры инкапсулируют то, как состояние приложения меняется в ответ на определенные действия, которые управляют хранилищем данных.
1
2
3
4
5
6
| function myReducer(previousState, action) => {
// use the action type and payload
// to create a new state based on
// the previous state.
return newState;
}
|
Так как редьюсеры должны быть читсыми функциями, внутри них нельзя использовать обращения к API, навигационные вызовы, вызывать нечитые функции (Math.random(), Date.now()), ну и конечно, напрямую обращаться к интерфейсу приложения.
Если в приложении используется несколько состояний, то можно иметь несколько редьюсеров.
Действия - это объекты JavaScript, у которых есть обязательное поле type
и необязательное payload
. Они представляют собой запросы, которые приложение отправляет к хранилищу. Большинство изменений состояния приложения инициируются пользователем (прямо или косвенно) - тап по кнопке, выбор пункта меню, AJAX-запрос и так далее. Даже первоначальная заргузка страницы может быть связана с действием. Дейтсвия часто создаются генераторами действий.
Генератор действия - это функция, которая возвращает объект действия. Это может показаться излишним, но такой полход повышает переносимость и тестируемость приложений.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| export function addTodo({ task }) {
return {
type: 'ADD_TODO',
payload: {
task,
completed: false
},
}
}
// example returned value:
// {
// type: 'ADD_TODO',
// todo: { task: '🛒 get some milk', completed: false },
// }
|
Объект действия, возвращенный из генератора затем передается всем редьюсерам, используемым в приложении. Если конкретный редьюсер обрабатывает данное действие, то он возвращает новое состояние (или часть состояния), а если нет - то прежнее. Новое состояние автоматически передается в приложение React, а оттуда вызывается перерендеринг всех конпонентов.
1
2
3
4
5
6
7
8
9
10
11
12
13
| export default function(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
const newState = [...state, action.payload];
return newState;
// Deal with more cases like
// 'TOGGLE_TODO', 'DELETE_TODO',...
default:
return state;
}
}
|
Создание редьюсера
1
| npm install redux react-redux
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // FriendsReducer.js
import { combineReducers } from 'redux';
const INITIAL_STATE = {
current: [],
possible: [
'Alice',
'Bob',
'Sammy',
],
};
const friendsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
default:
return state
}
};
export default combineReducers({
friends: friendsReducer
});
|
1
2
3
4
5
6
7
8
| // FriendsActions.js
export const addFriend = friendsIndex => (
{
type: 'ADD_FRIEND',
payload: friendsIndex,
}
);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // FriendsReducer.js
// ...
const friendsReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'ADD_FRIEND':
// Pulls current and possible out of previous state
// We do not want to alter state directly in case
// another action is altering it at the same time
const {
current,
possible,
} = state;
// Pull friend out of friends.possible
// Note that action.payload === friendIndex
const addedFriend = possible.splice(action.payload, 1);
// And put friend in friends.current
current.push(addedFriend);
// Finally, update the redux state
const newState = { current, possible };
return newState;
default:
return state
}
};
// ...
|
Добавление редьюсера к приложению
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // App.js
// ...
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import friendsReducer from './FriendsReducer';
const store = createStore(friendsReducer);
class App extends React.Component {
// ...
render() {
return (
<Provider store={store}>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={HomeScreen}
/>
<Stack.Screen
name="Friends"
component={FriendsScreen}
/>
</Stack.Navigator>
</NavigationContainer>
</Provider>
)
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // HomeScreen.js
// ...
import { connect } from 'react-redux';
// ...
<Text>You have {
this.props.friends.current.length
} friends.</Text>
// ...
const mapStateToProps = (state) => {
const { friends } = state
return { friends }
};
export default connect(mapStateToProps)(HomeScreen);
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| // FriendsScreen.js
// ...
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { addFriend } from './FriendsActions';
// ...
{
this.props.friends.possible.map((friend, index) => (
<Button
key={ friend }
title={ `Add ${ friend }` }
onPress={() =>
this.props.addFriend(index)
}
/>
))
}
// ...
const mapStateToProps = (state) => {
const { friends } = state
return { friends }
};
const mapDispatchToProps = dispatch => (
bindActionCreators({
addFriend,
}, dispatch)
);
export default connect(mapStateToProps, mapDispatchToProps)(FriendsScreen);
|
Использование Fetch API
1
2
3
4
5
6
7
8
9
10
11
| fetch('https://mywebsite.com/endpoint/', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
firstParam: 'yourValue',
secondParam: 'yourOtherValue'
})
});
|
1
2
3
4
5
6
7
8
9
10
| const getMoviesFromApi = () => {
return fetch('https://reactnative.dev/movies.json')
.then((response) => response.json())
.then((json) => {
return json.movies;
})
.catch((error) => {
console.error(error);
});
};
|
1
2
3
4
5
6
7
8
9
10
11
| const getMoviesFromApiAsync = async () => {
try {
const response = await fetch(
'https://reactnative.dev/movies.json'
);
const json = await response.json();
return json.movies;
} catch (error) {
console.error(error);
}
};
|