Взаимодействуем с VK
Чтобы работать с VK API вам необходимо будет создать приложение на сайте vk.com, и указать в настройках URL сервера, с которого вы будете выполнять запросы.
По адресу https://vk.com/apps?act=manage создайте новое приложение (веб-сайт) и заполните поля как на скриншоте, если используете локалхост и порт 3000.
vk-app-create

Интеграция VK API

Необходимо добавить скрипт openapi (документация), а так же вызвать VK.init
1
<!DOCTYPE html>
2
<html lang="en">
3
<head>
4
<meta charset="utf-8">
5
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
<meta name="theme-color" content="#000000">
7
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
8
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
9
<title>Redux [RU] Tutorial v2</title>
10
</head>
11
<body>
12
<noscript>
13
You need to enable JavaScript to run this app.
14
</noscript>
15
<div id="root"></div>
16
<script src="https://vk.com/js/api/openapi.js?158"></script>
17
<script language="javascript">
18
VK.init({
19
apiId: XXXXXX <!-- ваш номер -->
20
});
21
</script>
22
</body>
23
</html>
Copied!
Номер приложения можно посмотреть здесь:
app-number

Авторизация

Создадим действия для User.
src/actions/UserActions.js
1
export const LOGIN_REQUEST = 'LOGIN_REQUEST'
2
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
3
export const LOGIN_FAIL = 'LOGIN_FAIL'
4
5
export function handleLogin() {
6
return function(dispatch) {
7
dispatch({
8
type: LOGIN_REQUEST,
9
})
10
11
//eslint-disable-next-line no-undef
12
VK.Auth.login(r => {
13
if (r.session) {
14
let username = r.session.user.first_name
15
16
dispatch({
17
type: LOGIN_SUCCESS,
18
payload: username,
19
})
20
} else {
21
dispatch({
22
type: LOGIN_FAIL,
23
error: true,
24
payload: new Error('Ошибка авторизации'),
25
})
26
}
27
}, 4) // запрос прав на доступ к photo
28
}
29
}
Copied!
Так как загрузка информации из профиля - действие асинхронное, мы использовали проверенную схему из трех действий:
  • XXX_REQUEST - диспатчим непосредственно перед стартом реального запроса (для юзера это выглядит, как будто во время запроса)
  • XXX_SUCCESS + данные - если все прошло успешно добавляем данные [1]
  • ХХХ_FAIL + ошибка - если что-то пошло не так
[1] Чтобы достать имя пользователя, мы вытащили его из response(r).session. Данные нам предоставил VK, так как мы подтвердили "разрешаю доступ" во всплывающем окне.
vk-app-console-log-response
"Приконнектим" в <App /> UserActions, и добавим новые свойства в компонент <User />
src/containers/App.js
1
import React, { Component } from 'react'
2
import { connect } from 'react-redux'
3
import { User } from '../components/User'
4
import { Page } from '../components/Page'
5
import { getPhotos } from '../actions/PageActions'
6
import { handleLogin } from '../actions/UserActions'
7
8
class App extends Component {
9
render() {
10
// вытащили handleLoginAction из this.props
11
const { user, page, getPhotosAction, handleLoginAction } = this.props
12
return (
13
<div className="app">
14
<Page
15
photos={page.photos}
16
year={page.year}
17
isFetching={page.isFetching}
18
getPhotos={getPhotosAction}
19
/>
20
{/* добавили новые props для User */}
21
<User
22
name={user.name}
23
isFetching={user.isFetching}
24
error={user.error}
25
handleLogin={handleLoginAction}
26
/>
27
</div>
28
)
29
}
30
}
31
32
const mapStateToProps = store => {
33
return {
34
user: store.user, // вытащили из стора (из редьюсера user все в переменную thid.props.user)
35
page: store.page,
36
}
37
}
38
39
const mapDispatchToProps = dispatch => {
40
return {
41
getPhotosAction: year => dispatch(getPhotos(year)),
42
// "приклеили" в this.props.handleLoginAction функцию, которая умеет диспатчить handleLogin
43
handleLoginAction: () => dispatch(handleLogin()),
44
}
45
}
46
47
export default connect(
48
mapStateToProps,
49
mapDispatchToProps
50
)(App)
Copied!
Здесь мы поступили так же, как когда-то для page:
  • подписались на кусочек стора (user)
  • добавили экшен и передали его в dispatch в функции handleLoginAction
  • кусочек стора (user) и handleLoginAction - стали доступны нам в this.props
  • в <User /> передали необходимые свойства
Обновим reducer user:
src/reducers/user.js
1
import { LOGIN_REQUEST, LOGIN_SUCCESS, LOGIN_FAIL } from '../actions/UserActions'
2
3
const initialState = {
4
name: '',
5
error: '', // добавили для сохранения текста ошибки
6
isFetching: false, // добавили для реакции на статус "загружаю" или нет
7
}
8
9
export function userReducer(state = initialState, action) {
10
switch (action.type) {
11
case LOGIN_REQUEST:
12
return { ...state, isFetching: true, error: '' }
13
14
case LOGIN_SUCCESS:
15
return { ...state, isFetching: false, name: action.payload }
16
17
case LOGIN_FAIL:
18
return { ...state, isFetching: false, error: action.payload.message }
19
20
default:
21
return state
22
}
23
}
Copied!
В редьюсере есть интересные моменты:
  • когда мы начали делать запрос (LOGIN_REQUEST) мы очищаем error. Например, была ошибка, мы стали делать новый запрос - ошибка очистилась;
  • если случился LOGIN_SUCCESS - мы в name записываем action.payload (а как вы помните, там мы передаем в строке имя пользователя) и ставим статус загрузки - false (то есть, не загружается, ибо загрузилось);
  • если случился LOGIN_FAIL - опять же, загружаю? Нет, значит isFetching - false. Ошибка? Да - запиши в поле error.
Прокачаем <User />:
src/components/User.js
1
import React from 'react'
2
import PropTypes from 'prop-types'
3
4
export class User extends React.Component {
5
renderTemplate = () => {
6
const { name, error, isFetching } = this.props
7
8
if (error) {
9
return <p>Во время запроса произошла ошибка, обновите страницу</p>
10
}
11
12
if (isFetching) {
13
return <p>Загружаю...</p>
14
}
15
16
if (name) {
17
return <p>Привет, {name}!</p>
18
} else {
19
return (
20
<button className="btn" onClick={this.props.handleLogin}>
21
Войти
22
</button>
23
)
24
}
25
}
26
render() {
27
return <div className="ib user">{this.renderTemplate()}</div>
28
}
29
}
30
31
User.propTypes = {
32
name: PropTypes.string.isRequired,
33
error: PropTypes.string,
34
isFetching: PropTypes.bool.isRequired,
35
handleLogin: PropTypes.func.isRequired,
36
}
Copied!
В коде компонента <User /> ничего необычного нет. Рендерим шаблончик (в зависимости от props).
Сейчас если кликнуть на "войти" - всплывет VK окно с подтверждением прав доступа (первый раз). После подтверждения прав, вместо кнопки войти появляется надпись "Привет, ХХХ". При перезагрузке сайта и повторных нажатиях на "войти" - VK окно мгновенно закрывается, а кнопка вновь изменяется на "Привет, XXX".
Как всегда, доблестный логгер пишет в консоли - что происходит.
vk-app-login-success

Загрузка фото

Нам нужно практически повторить, все что написано выше, только для блока Page.
Поэтому, наконец-то появилась самостоятельная задача. Я крайне рекомендую с ней посидеть, так как это практически конец основного материала. Если у вас что-то не получится - вы поймете что нужно закрепить, что перечитать. Не торопитесь смотреть ответ, попробуйте сделать это сами, таким образом вы получите от этого учебника гораздо больше. Да и кайфово это :)
Задача: используя метод photos.getAll вытащите свои фотографии из VK за год, выбранный кнопкой. Отсортируйте их в обратном порядке по лайкам, чтобы самая популярная фото оказалась первой.
После скриншотов есть подсказка: функция, которая делает запрос за фото.
Должно выглядеть следующим образом:
vk-app-photo-by-likes
Подсказка: функция для загрузки фото
1
let photosArr = []
2
let cached = false
3
4
function makeYearPhotos(photos, selectedYear) {
5
let createdYear,
6
yearPhotos = []
7
8
photos.forEach(item => {
9
createdYear = new Date(item.date * 1000).getFullYear()
10
if (createdYear === selectedYear) {
11
yearPhotos.push(item)
12
}
13
})
14
15
yearPhotos.sort((a, b) => b.likes.count - a.likes.count)
16
17
return yearPhotos
18
}
19
20
function getMorePhotos(offset, count, year, dispatch) {
21
//eslint-disable-next-line no-undef
22
VK.Api.call(
23
'photos.getAll',
24
{ extended: 1, count: count, offset: offset, v: '5.80' },
25
r => {
26
try {
27
photosArr = photosArr.concat(r.response.items)
28
if (offset <= r.response.count) {
29
offset += 200 // максимальное количество фото которое можно получить за 1 запрос
30
getMorePhotos(offset, count, year, dispatch)
31
} else {
32
let photos = makeYearPhotos(photosArr, year)
33
cached = true
34
dispatch({
35
type: GET_PHOTOS_SUCCESS,
36
payload: photos,
37
})
38
}
39
} catch (e) {
40
dispatch({
41
type: GET_PHOTOS_FAIL,
42
error: true,
43
payload: new Error(e),
44
})
45
}
46
}
47
)
48
}
Copied!
Так как я не нашел опцию передачи года, то просто выгрузил все фото, по 200 штук за один запрос. Это несколько избыточно, как и тот факт, что мы вызываем функцию makeYearPhotos, вместо того чтобы один раз загрузить все фото и "разместить" их по годам. Я оставил код из первого издания учебника, чтобы не усложнять пример.
Решение ниже:
.
.
.
.

Решение:

src/actions/PageActions.js
1
export const GET_PHOTOS_REQUEST = 'GET_PHOTOS_REQUEST'
2
export const GET_PHOTOS_SUCCESS = 'GET_PHOTOS_SUCCESS'
3
export const GET_PHOTOS_FAIL = 'GET_PHOTOS_FAIL'
4
5
let photosArr = []
6
let cached = false
7
8
function makeYearPhotos(photos, selectedYear) {
9
let createdYear,
10
yearPhotos = []
11
12
photos.forEach(item => {
13
createdYear = new Date(item.date * 1000).getFullYear()
14
if (createdYear === selectedYear) {
15
yearPhotos.push(item)
16
}
17
})
18
19
yearPhotos.sort((a, b) => b.likes.count - a.likes.count)
20
21
return yearPhotos
22
}
23
24
function getMorePhotos(offset, count, year, dispatch) {
25
//eslint-disable-next-line no-undef
26
VK.Api.call(
27
'photos.getAll',
28
{ extended: 1, count: count, offset: offset, v: '5.80' },
29
r => {
30
try {
31
photosArr = photosArr.concat(r.response.items)
32
if (offset <= r.response.count) {
33
offset += 200 // максимальное количество фото которое можно получить за 1 запрос
34
getMorePhotos(offset, count, year, dispatch)
35
} else {
36
let photos = makeYearPhotos(photosArr, year)
37
cached = true
38
dispatch({
39
type: GET_PHOTOS_SUCCESS,
40
payload: photos,
41
})
42
}
43
} catch (e) {
44
dispatch({
45
type: GET_PHOTOS_FAIL,
46
error: true,
47
payload: new Error(e),
48
})
49
}
50
}
51
)
52
}
53
54
export function getPhotos(year) {
55
return dispatch => {
56
dispatch({
57
type: GET_PHOTOS_REQUEST,
58
payload: year,
59
})
60
61
if (cached) {
62
let photos = makeYearPhotos(photosArr, year)
63
dispatch({
64
type: GET_PHOTOS_SUCCESS,
65
payload: photos,
66
})
67
} else {
68
getMorePhotos(0, 200, year, dispatch)
69
}
70
}
71
}
Copied!
makeYearPhotos и getMorePhotos можно вынести в папку utils, как вспомогательные функции.
Главное здесь, что мы по прежнему вызываем действия (dispatch actions). Все так, как было в самом начале, просто добавилось немного больше логики для получения фото. Алгоритм получения всех фото (да и необходимость получения всех) - оставляю без комментариев. Мне кажется, это приемлемый способ.
Исправив редьюсер и отрисовку в компоненте, мы закончим начатое.
src/reducers/page.js
1
import {
2
GET_PHOTOS_REQUEST,
3
GET_PHOTOS_SUCCESS,
4
GET_PHOTOS_FAIL,
5
} from '../actions/PageActions'
6
7
const initialState = {
8
year: 2018,
9
photos: [],
10
isFetching: false,
11
error: '',
12
}
13
14
export function pageReducer(state = initialState, action) {
15
switch (action.type) {
16
case GET_PHOTOS_REQUEST:
17
return { ...state, year: action.payload, isFetching: true, error: '' }
18
19
case GET_PHOTOS_SUCCESS:
20
return { ...state, photos: action.payload, isFetching: false, error: '' }
21
22
case GET_PHOTOS_FAIL:
23
return { ...state, error: action.payload.message, isFetching: false }
24
25
default:
26
return state
27
}
28
}
Copied!
src/components/Page.js
1
import React from 'react'
2
import PropTypes from 'prop-types'
3
4
export class Page extends React.Component {
5
onBtnClick = e => {
6
const year = +e.currentTarget.innerText
7
this.props.getPhotos(year) // setYear -> getPhotos
8
}
9
renderTemplate = () => {
10
const { photos, isFetching, error } = this.props
11
12
if (error) {
13
return <p className="error">Во время загрузки фото произошла ошибка</p>
14
}
15
16
if (isFetching) {
17
return <p>Загрузка...</p>
18
} else {
19
return photos.map((entry, index) => ( // [1]
20
<div key={index} className="photo">
21
<p>
22
<img src={entry.sizes[0].url} alt="" />
23
</p>
24
<p>{entry.likes.count}</p>
25
</div>
26
))
27
}
28
}
29
30
render() {
31
const { year, photos } = this.props
32
return (
33
<div className="ib page">
34
<p>
35
<button className="btn" onClick={this.onBtnClick}>
36
2018
37
</button>{' '}
38
<button className="btn" onClick={this.onBtnClick}>
39
2017
40
</button>{' '}
41
<button className="btn" onClick={this.onBtnClick}>
42
2016
43
</button>{' '}
44
<button className="btn" onClick={this.onBtnClick}>
45
2015
46
</button>{' '}
47
<button className="btn" onClick={this.onBtnClick}>
48
2014
49
</button>
50
</p>
51
<h3>
52
{year} год [{photos.length}]
53
</h3>
54
{this.renderTemplate()}
55
</div>
56
)
57
}
58
}
59
60
Page.propTypes = {
61
year: PropTypes.number.isRequired,
62
photos: PropTypes.array.isRequired,
63
getPhotos: PropTypes.func.isRequired,
64
error: PropTypes.string,
65
isFetching: PropTypes.bool.isRequired,
66
}
Copied!
[1] - как вы заметили, мы использовали index в качестве ключа для наших div'ов. Запустите пример, попробуйте поменять года. Возможно, вы словите баг, когда у элементов с одинаковым индексом изображение меняется с задержкой. Проблема в том, что мы использовали индекс для элементов, которые изменяются (а индекс-то остается прежним! Ключ в итоге не изменяется, итого реакт "путается").
Чтобы этого избежать, сделайте ключ уникальным (например, для этого у нас есть id в ответе от VK API):
1
if (isFetching) {
2
return <p>Загрузка...</p>
3
} else {
4
return photos.map(entry => (
5
<div key={entry.id} className="photo">
6
<p>
7
<img src={entry.sizes[0].url} alt="" />
8
</p>
9
<p>{entry.likes.count}</p>
10
</div>
11
))
12
}
Copied!
Теперь наш ключ (key = {entry.id}) уникальный и бага нет.
Мини-задачка на внимательность: если сейчас сгенерировать ошибку, то ничего не отобразиться. Как это исправить?
Чтобы проверить ошибку, сделайте в функции запроса фото, поставьте count: -1:
src/actions/PageActions.js
1
...
2
function getMorePhotos(offset, count, year, dispatch) {
3
//eslint-disable-next-line no-undef
4
VK.Api.call(
5
'photos.getAll',
6
{ extended: 1, count: -1, offset: offset, v: '5.80' },
7
r => {
8
...
Copied!
Проблема:
vk-app-error-not-displayed
Решение:
1
...
2
3
class App extends Component {
4
render() {
5
const { user, page, getPhotosAction, handleLoginAction } = this.props
6
return (
7
<div className="app">
8
{/* добавили error prop для Page */}
9
<Page
10
photos={page.photos}
11
year={page.year}
12
isFetching={page.isFetching}
13
error={page.error}
14
getPhotos={getPhotosAction}
15
/>
16
...
17
}
18
19
...
Copied!
vk-app-error-displayed
Итого: закрепили работу с асинхронными запросами.
Исходный код на текущий момент.
P.S. css тоже был слегка подправлен.