Серия блогов React: часть четвертая

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

Серия блогов React

Часть первая: Создание веб-сайта с помощью React и Bulma
Часть вторая: Создание блога с помощью React и Contentful
Часть третья. Импортируйте свой канал Medium в React
Часть четвертая: Добавление Redux в блог React
Часть пятая: Замена Redux Thunks на Redux Sagas
Часть шестая: (В процессе) Написание модульного теста для блога React с Redux Sagas

Вот он, момент, которого вы все ждали. - Redux !!!

npm install react-redux redux-thunk redux-immutable-state-invariant

Примечание. Вы также можете установить redux в качестве зависимости от разработчика. Ходят слухи, что пряжа решает эту проблему, но вы также можете вручную установить ее на дочерние предприятия. npm install redux --save-dev

Добавление магазина

Так что это может быть непопулярное мнение, потому что я знаю, что всем тяжело от ReDucks, но это неправильный способ делать это. Когда вы помещаете файлы магазина вместе со своими компонентами, вы объединяете уровень презентации с бизнес-уровнем и состоянием приложения. Если появится что-то новое, заменяющее Redux, вам не придется удалять файлы из всех наших папок, просто удалите весь магазин. Но мы отделим наш тип файла в магазине.

Концепция фрактала

Помните, что мы используем концепции ключевого значения фрактальной файловой структуры. В магазине мы будем следовать этому шаблону с key = public actions (thunks) и value = private (types reducer and actions), что означает, что наше приложение никогда не будет разговаривать. в файлы в нашей папке. Они будут разговаривать только с преобразователем (ключ, общедоступные действия), который будет взаимодействовать с файлами в своей папке (значение, частные классы). В большинстве случаев наш преобразователь будет откуда-то получать данные в наш магазин.

Еще не запутались? Я так думал.

Вот пример файловой структуры хранилища пользователей. Наши компоненты будут взаимодействовать только с User.js, который, в свою очередь, обращается к файлам в папке пользователя. Со временем в этом будет больше смысла.

src 
- app 
  - ... app code ...
- store
  - User.js
  - user
    - actions
    - reducer
    - types

Настраивать

В папке src добавьте папку под названием store.

Затем добавьте три файла: index.js, initialState.js и rootReducer.js.

Настроить index.js

import { createStore, applyMiddleware } from 'redux'
import { rootReducer } from './rootReducer'
import reduxImmutableStateInvariant from 'redux-immutable-state-invariant'
import thunk from 'redux-thunk'
export function configureStore(initialState) {
  return createStore(
    rootReducer,
    initialState,
    applyMiddleware(
      thunk,
      reduxImmutableStateInvariant()
    )
  )
}

Настроить initialState.js

export default {
}

Установите rootReducer.js

import { combineReducers } from 'redux'
export const rootReducer = combineReducers({
})

Чтобы подключиться к приложению, откройте приложения index.js в папке src, а также магазин и оберните приложение.

import React from 'react';
import ReactDOM from 'react-dom'
import registerServiceWorker from './registerServiceWorker'
import { BrowserRouter as Router } from 'react-router-dom'
import App from './App'
// Redux Store
import { Provider } from 'react-redux'
import { configureStore } from './src/store'
import './index.css'
const store = configureStore()
ReactDOM.render((
  <Provider store={store}>
    <Router>
      <App />
    </Router>
  </Provider>
), document.getElementById('root'))
registerServiceWorker()

ConfigureStore - это index.js нашего Магазина, в настоящее время мы не передаем никакого начального состояния во время настройки.

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

Redux Blog Store

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

В папке нашего магазина создайте файл под названием Blog.js и папку под названием блог. В папке блога создайте три заливки: actions.js, reducer.js и types.js.

Начнем с наших типов. Это константы, которые наши действия используют для отправки вызовов. Во-первых, нам просто нужно загрузить данные нашего блога в наш магазин.

В store/blog/types.js добавить

/**
* Blog Types
*/
export const LOAD_BLOG_SUCCESS = 'LOAD_BLOG_SUCCESS'

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

В store/initialState.js добавить

export default {
  blog: {
    posts: []
  }
}

Затем давайте настроим наш редуктор. Это то, что обновляет наше состояние, в котором хранятся наши данные. Действия будут вызывать их, и с помощью оператора switch он будет искать тип, который был вызван действием.

В store/blog/reducer.js добавить

/**
* Blog Reducer
*/
import initialState from '../../store/initialState'
import * as types from './types'
export default function blogReducer(state = initialState.blog, action) {
  switch (action.type) {
    case types.LOAD_BLOG_POSTS_SUCCESS:
      return {
        ...state,
        posts: action.posts
      } 
    default:
      return state
  }
}

Затем нам нужно настроить действие для отправки типа и установить его данные в качестве полезной нагрузки (сообщений).

В store/blog/actions.js добавить

/**
* Blog Actions
*/
import * as types from './types'
export function loadBlogSuccess(post) {
  return { type: types.LOAD_BLOG_SUCCESS, post}
}

Теперь нам нужно включить это в наш rootReduce. rootReducer объединяет все редукторы хранилища в один гигантский мега-редуктор, который создает глобальное состояние, доступное везде в приложении.

В store/rootReducer.js добавить

import { combineReducers } from 'redux'
import blog from './blog/reducer'
export const rootReducer = combineReducers({
  blog
})

Обратите внимание, что мы не импортировали его как blogReducer, потому что, как бы вы ни называли его в combReduces, вы получаете к нему доступ в остальной части приложения. Экспорт по умолчанию - это то, что позволяет нам называть вещи как угодно при импорте функции по умолчанию. Мы могли бы импортировать его как blogReducer, а затем выполнить функции combReducers как blog: blogReducer, но я лично считаю, что это выглядит лучше. #CodeIsArt

Проверка того, что Redux работает

Прямо сейчас наш код работает. Он пока ничего не делает, потому что на самом деле не делает того, что мы видим, но он работает и я могу это доказать.

Для начала вам понадобится расширение Redux Dev Tools для Chrome. Https://github.com/zalmoxisus/redux-devtools-extension

npm install --save-dev redux-devtools-extension

Затем нам нужно его подключить. В store/index.js мы собираемся удалить initialState, потому что мы будем устанавливать их в редукторах каждого хранилища, мы обернем наше промежуточное ПО функцией composeWithDevTools.

import { createStore, applyMiddleware } from 'redux'
import { rootReducer } from './rootReducer'
import reduxImmutableStateInvariant from 'redux-immutable-state-invariant'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension';
export function configureStore() {
  return createStore(
    rootReducer,
    composeWithDevTools(
      applyMiddleware(
        thunk,
        reduxImmutableStateInvariant()
      )
    )
  )
}

Затем установите расширение в Chrome и откройте инспектор Chrome (обычно я щелкаю правой кнопкой мыши по странице и нажимаю «Инспектор»). В верхней части навигации теперь у вас должна быть опция Redux.

Щелкните его и обновите приложение. Вкладка Redux иногда должна быть открыта при загрузке приложения, чтобы оно работало, потому что соединение устанавливается при загрузке сайта в функции createStore (…).

Теперь вы должны увидеть свое состояние и историю Redux. По умолчанию используется Различие, поэтому перейдите на вкладку Состояние, и вы должны увидеть объект по умолчанию.

Довольно круто, правда!

Размещение данных блога в магазине блогов

Помните, что в файле Blog.js есть публичные действия (преобразователь) нашего магазина. Именно сюда мы будем помещать наши преобразователи и извлекать наши данные при загрузке сайта.

Мы можем повторно использовать часть нашей логики из app/Blog.js, потому что ему больше не нужно будет вызывать Contentful. Это произойдет в нашем thunk.

Давайте настроим наш преобразователь. Откройте store/Blog.js и добавьте

import * as contentful from 'contentful'
import * as actions from './blog/actions'
const client = contentful.createClient({
  space: 'qu10m4oq2u62',
  accessToken: 'f4a9f68de290d53552b107eb503f3a073bc4c632f5bdd50efacc61498a0c592a'
})
const error = err => console.log(err)
export function loadBlog() {
  return dispatch =>
    client.getEntries()
      .then(({items}) => {
        dispatch(actions.loadBlogSuccess(items))
      })
      .catch(error)
}

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

Этот код вызовет Contentful и отправит эти данные в действия loadBlogSuccess, которые будут хранить данные о состоянии нашего блога под сообщениями.

Но это не произойдет автоматически, нам нужно будет вызвать эту функцию при загрузке сайта.

Мы сделаем это, добавив строку в наш корневой index.js файл

...
// Redux Store
import { Provider } from 'react-redux'
import { configureStore } from './store'
import { loadBlog } from './store/Blog'
import './index.css'
const store = configureStore()
store.dispatch(loadBlog())
ReactDOM.render((
...

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

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

Использование магазина в компоненте

Это довольно простой шаг. Redux предоставляет нам функцию, которую мы можем использовать, называемую mapStateToProps, которая, как вы, вероятно, догадались, сопоставляет наше состояние redux ... с нашими реквизитами страницы. Затем мы «подключаем» mapStateToProps и Blog.js к Redux.

Вот и вся магия: откройте app / Blog.js и удалите наш старый код, чтобы получать сообщения и добавлять / обновлять 8 строк кода.

import React from 'react'
import { connect } from 'react-redux'
import * as contentful from 'contentful'
import BlogItem from './blog/BlogItem'
import PageHeader from './components/PageHeader'
import PageContent from './components/PageContent'
class Blog extends React.Component {
  render() {
    return (
      <div>
        <PageHeader color="is-info" title="Code Blog">
          Your standard <strong>JavaScript</strong> programming blog, albeit, probably not very good, but I will at least try to keep it entertaining. This blog is a chronological mix of random posts on Angular, React, Functional Programming, and my <strong>project walkthroughs</strong>.
        </PageHeader>
        <PageContent>
          { this.props.blog.posts.map(({fields}, i) =>
            <BlogItem key={i} {...fields} />
          )}
        </PageContent>
      </div>
    )
  }
}
function mapStateToProps(state, ownProps) {
  return {
    blog: state.blog
  }
}
export default connect(mapStateToProps)(Blog)

Бламмо! Redux!

Правильное использование состояния блога

Я буду первым, кто признает, что, когда я впервые начал использовать Redux, я использовал его как хранилище данных, и это намного больше. Это состояние вашего приложения, где оно движется, где оно было, что делает, что знает… Использование его только для хранения данных является оскорблением его существования. Например, наше приложение сейчас работает нормально, потому что Contentful потрясающий и быстрый, и у нас очень мало данных, но что, если бы у нас было много данных или медленное соединение. мы не хотим загружать пустую игру для пользователя.

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

магазин / initialState.js

export default {
  blog: {
    loading: false,
    posts: []
  }
}

магазин / блог / types.js

/**
* Blog Types
*/
export const BLOG_LOADING = 'BLOG_LOADING'
export const LOAD_BLOG_SUCCESS = 'LOAD_BLOG_SUCCESS'

магазин / блог / reducer.js

/**
* Blog Reducer
*/
import initialState from '../../store/initialState'
import * as types from './types'
export default function blogReducer(state = initialState.blog, action) {
  switch (action.type) {
    case types.BLOG_LOADING:
      return {
        ...state,
        loading: action.isLoading
      }
    case types.LOAD_BLOG_SUCCESS:
      return {
        ...state,
        posts: action.posts,
        loading: false
      }
   default:
     return state
  }
}

да. Состояние содержит только посты и загрузку, но мы все равно выкладываем его. Если вы ведете счет, возможно, заметили это раньше. Это хорошая практика, потому что мы каждый раз устанавливаем новый объект состояния. Сначала покупайте всегда распространяющееся состояние, мы получаем все предыдущие значения и не забываем ни одного, а затем новые поля просто перезаписывают предыдущие значения. Нравится Object.assign({}, previouseState, { …newStateValues})

магазин / блог / actions.js

/**
* Blog Actions
*/
import * as types from './types'
export function blogLoading(isLoading = true) {
  return { type: types.BLOG_LOADING, isLoading}
}
export function loadBlogSuccess(posts) {
  return { type: types.LOAD_BLOG_SUCCESS, posts}
}

Мы устанавливаем для isloading значение по умолчанию true, поэтому нам обычно не нужно передавать какие-либо данные при отправке blogLoading. НО, если мы можем передать что-то вроде false, это означает, что нам не нужно создавать действие, чтобы установить для загрузки значение false, например, если было ошибка с загрузкой.

store / Blog.js

import * as contentful from 'contentful'
import * as actions from './blog/actions'
const client = contentful.createClient({
  space: 'qu10m4oq2u62',
  accessToken: 'f4a9f68de290d53552b107eb503f3a073bc4c632f5bdd50efacc61498a0c592a'
})
export function loadBlog() {
  return dispatch => {
    dispatch(actions.blogLoading())
    return client.getEntries()
      .then(({items}) => {
        dispatch(actions.loadBlogSuccess(items))
      })
      .catch(error => {
        console.log(error)
        dispatch(actions.blogLoading(false))
      })
  }
}

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

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

Обновите представление, чтобы добавить значок загрузки

Вернемся к app/Blog.js, давайте настроим текст-заполнитель, чтобы убедиться, что наша логика работает.

<div>
  <PageHeader color="is-info" title="Code Blog">
Your standard <strong>JavaScript</strong> programming blog, albeit, probably not very good, but I will at least try to keep it entertaining. This blog is a chronological mix of random posts on Angular, React, Functional Programming, and my <strong>project walkthroughs</strong>.
  </PageHeader>
  { this.props.blog.loading
    ? <div>Loading</div>
    : <PageContent>
        { this.props.blog.posts.map(({fields}, i) =>
          <BlogItem key={i} {...fields} />
        )}
      </PageContent>
 }
</div>

Мы завершаем предыдущий код тернарным оператором для обработки оператора if. Это означает, что если загрузка верна, мы покажем загрузочный div, иначе мы покажем контент dev.

Если вы похожи на меня, Contentful работает слишком быстро, и вы его не видите. Мы можем заключить отправку в обещание успеха getEntries с тайм-аутом. setTimeout(() => dispatch(actions.loadBlogSuccess(items)), 5000)

Теперь вы должны увидеть загружаемый div.

Или вы можете использовать функцию перехода по истории в Redux Dev Tools. Я позволю тебе самому открыть для себя эту жемчужину;)

Теперь в app/components создайте Loader.js, который будет стилизованным компонентом для нашего счетчика.

Теперь вернемся к нашему app/Blog.js, давайте добавим счетчик. Заменить <div>Loading</div> на

<Loader className="has-text-primary"></Loader>

и не забудьте импортировать загрузчик import { Loader } from ‘./components/Loader’

По умолчанию загрузчик имеет белый цвет, поэтому вам нужно либо жестко закодировать цвет, либо с помощью Bulma, мы можем просто присвоить ему класс has-text- $ color (например, has-text -первичный) .

Нашел ошибку :(

Вернувшись на этап очистки Создание блога с React и Contentful, я забыл изменить текст для Подробнее и Вернуться в блог в меню элементов блога.

Это легко исправить с помощью простого тернарного оператора в <Link> с использованием свойства to.

В app/blog/shared/BlogNav.js можно Ссылку на это

<Link className="level-item button is-small is-link is-outlined" to={to}>
  { to === '/blog' ? 'Back to Blog' : 'Read More'}
</Link>

Добавить магазин на среднюю страницу

Теперь мы можем сделать то же самое для нашей страницы Medium. Я не собираюсь показывать вам каждый шаг, потому что мы учимся не так, но я перечислю их для вас.

Шаг 1: Создайте типы
Шаг 2: Добавьте состояние в исходное состояние
Шаг 3: Настройте редукторы
Шаг 4: настройка действия
Шаг 5: добавление редукторов в rootReducer
Шаг 6: настройка нашего преобразователя (подсказка ниже)

import * as actions from './medium/actions'
import axios from 'axios'
const fetchPosts = () => axios.get(`https://cors.now.sh/https://us-central1-aaronklaser-1.cloudfunctions.net/[email protected]`)
const setPosts = ({data}) => Object.values(data.payload.references.Post).map(
  ({ id, title, createdAt, virtuals, uniqueSlug }) => Object.assign({},{
    title,
    createdAt,
    subtitle: virtuals.subtitle,
    image: virtuals.previewImage.imageId ? `https://cdn-images-1.medium.com/max/800/${virtuals.previewImage.imageId}` : null,
    url: `https://medium.com/@aaron.klaser/${uniqueSlug}`
  })
)
export const loadMedium = () => async dispatch => {
  dispatch(actions.mediumLoading())
  const data = await fetchPosts()
  return dispatch(actions.loadMediumSuccess(setPosts(data)))
}

Шаг 7. Добавьте состояние к компоненту

Проверьте это и двигайтесь дальше по жизни. Теперь вы мастер Redux!

Давайте рассмотрим

  • Добавьте Redux и друзей в свой проект.
  • Настройте Магазин с использованием фрактальной файловой структуры
  • Настроить редуктор типов магазина и действия по загрузке данных
  • Настройте преобразователь магазина, который является нашим классом публичных действий.
  • Добавить магазин в компонент страницы
  • Добавлено состояние загрузки и анимация загрузки
  • Исправлена ​​ошибка предварительного просмотра
  • Добавлен магазин на страницу Medium

Далее - Замена Redux Thunks на Redux Sagas