Простейшее приложение

Стилизация компонентов

Использование стилей

Задание абсолютных размеров элементов

Задание размеров через flex

Задание относительных размеров

Атрибут flexDirection

Атрибут direction

Атрибут justifyContent

Атрибут alignSelf

Атрибут alignItems

Атрибут alignContent

Атрибут flexWrap

Интерактивные элементы

Нативные компоненты

Основные кнопки

Элементы Touchables

Элемент ScrollView

Анимация

Хуки

Хук состояния

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);
  }
};

Основы React native

Основы React native