Skip to content

Instantly share code, notes, and snippets.

@dsvgit
Last active February 10, 2019 14:53
Show Gist options
  • Save dsvgit/183523b2565e3ca4b3e5d18e3bed46ac to your computer and use it in GitHub Desktop.
Save dsvgit/183523b2565e3ca4b3e5d18e3bed46ac to your computer and use it in GitHub Desktop.

Redux

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

API:

  • createStore(reducer) - создать store
  • store.dispatch(action) - отправить экшен
  • store.subscribe(listener) - подписаться на изменения стейта
  • store.getState() - доставать

Пример:

const { createStore } = Redux;

// редьюсер описывает все возможные изменения стора, здесь стейт просто Number, может быть что угодно
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

// создаем стор
let store = createStore(counter)

// подписываемся
store.subscribe(() =>
  console.log(store.getState())
)

// используем
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });

https://codepen.io/anon/pen/NMPdbp

Формат экшенов:

Для того чтобы все подряд не передавать придумали формат: https://github.com/redux-utilities/flux-standard-action

{
    type, // тип, чтобы редьюсер знал как обработать
    payload, // нагрузка, данные
    error, // есть ли ошибка, хз не испльзовал
    meta // доп инфа, хз не испльзовал
}

Ну и чтобы не писать store.dispatch({ type: 'INCREMENT' }); Придумали создавать функции - они называются actionCreators т.к. создают простые объекты экшенов:

function increment() {
    return { type: 'INCREMENT' }; // возвращает простой объект
}
store.dispatch(increment())

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

Умеем хранить данные, менять их откуда угодно, читать, подписываться на изменения - надо как то связать с React.

// сначала все тоже самое
const { createStore } = Redux;

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

let store = createStore(counter)

class App extends React.Component {
  state = {
    counter: store.getState()
  }
  
  componentDidMount = () => {
    // после маунта подписываемся на изменение стора, в подписке сетим стейт в counter (дублируем в компоненте), this.setState перерендеривает компонент с новым значением
    store.subscribe(() => this.setState({ counter: store.getState() }));
  }
  
  increment = (event) => {
    store.dispatch({ type: 'INCREMENT' });
  }
  
  decrement = (event) => {
    store.dispatch({ type: 'DECREMENT' });
  }
  
  render() {
    return (
      <div> 
        {this.state.counter}
        <button onClick={this.decrement}>-</button>
        <button onClick={this.increment}>+</button>
      </div>
    )
  }  
}

ReactDOM.render(
  <App />,
  document.getElementById("app")
);

https://codepen.io/anon/pen/dePNpB

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

Но такой способ слишком сложный и не много функций у него. Написали для таких подключений либу react-redux.

react-redux

API:

    - <Provider store={store}> - оборачивает все прирложение и в context запихивает туда store
    - connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]) - достает из контекста store, подписывается на него, мапит state на то, что из него нужно достать для конкретного компонента, connect это HOC

https://reactjs.org/docs/higher-order-components.html https://reactjs.org/docs/context.html теперь не нужно везде импортить store, чтобы подписаться или кинуть экшен. Уже все подписано - меняется стор и сразу перерендеривается все приложение.

Тот же пример с connect:

// все тоже самое
const { createStore, bindActionCreators } = Redux;
const { Provider, connect } = ReactRedux;

function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1
  case 'DECREMENT':
    return state - 1
  default:
    return state
  }
}

let store = createStore(counter);

class App extends React.Component {
  // уже нет никаких подписок, все подписано в родителе AppContainer
  render() {
    const { counter, increment, decrement } = this.props; // просто принимаем что пришло из родителя и используем
    
    return (
      <div> 
        {counter}
        <button onClick={decrement}>-</button>
        <button onClick={increment}>+</button>
      </div>
    )
  }  
}

// теперь все что касается подписок здесь AppContainer хранит дубликат стейта
const AppContainer = connect( // коннект примет стор, который ему передаст Provider и все сделает
  function mapStateToProps(state, props) { // мапим весь стейт на то что нужно этому компоненту
    return {
      counter: state
    }
  },
  function mapDispatchToProps(dispatch) { // мапим все экшены на то что нужно этому компоненту
		return {
      increment: () => dispatch({ type: 'INCREMENT' }),
      decrement: () => dispatch({ type: 'DECREMENT' })      
    };
	}
)(App);

ReactDOM.render(
  <Provider store={store}> // Provider передаст стор в context чтобы connect смог его принять и сделать все подписки и достать все данные для конкретного компонента
    <AppContainer />
  </Provider>,
  document.getElementById("app")
);

https://codepen.io/anon/pen/bMNgzY

Теперь по всему приложению можно коннектить любой компонент к любой части стора и менять из любого компонента любую часть стора.

redux thunk

Сейчас асинхронную логику можно писать внутри компонентов. Например при обработке клика. Или на componentDidMount и т.д.

<button onClick={() => getData().then(() => this.props.increment())}> // increment отарботает только после получения данных
   Получить данные 
</button>

Чтобы вынести подобную логику из компонентов придумали redux-thunk. https://github.com/gaearon/redux-thunk

теперь можно писать экшены как функции, которые принимают dispatch и getState установка: https://github.com/gaearon/redux-thunk#installation

пример таких экшенов:

function fetchUser(id) {
  return (dispatch, getState, api) => { // теперь не просто объект, теперь функция, есть dispatch, теперь прямо из экшена можно диспатчить и полчать стор
    fetchUsersApi(getState().userData.userId).then((response) => {
        dispatch(setUsers(response.data.users)); // после получения данных можно вызвать что угодно, напрмер записать юзеров в стор, чтобы отобразить гдето в компоненте
	// передается getState как функция потому, что тут все асинхронно и в этот момент (после получения данных) getState вернет актуальный state т.к. он уже мог поменятся 
	// можно еще задиспатчить что нибудь тут
	dispatch(showNotification({ text: 'users fetched' }));
    })
  }
}

function setUsers(users) {
    return { type: 'SET_USERS', payload: users };
}

Редьюсеры

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

Например есть стейт в котором есть userData и pageData { userData: { name }, pageData: { title } }

и редьюсер:

function reducer(state, action) {
  if (action.type == 'SET_NAME') {
    return { ...state, userData: { ...state.userData, { name: action.payload}}};
  }
  
  if (action.type == 'SET_TITLE') {
    return { ...state, pageData: { ...state.pageData, { title: action.payload}}};
  }
}

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

можно переписать:

function userDataReducer(state, action) {
  if (action.type == 'SET_NAME') {
    return { ...state, name: action.payload }; // рачитываем на то, что в state придет userData
  }
}

function pageDataReducer(state, action) {
  if (action.type == 'SET_TITLE') {
    return { ...state, title: action.payload }; // рачитываем на то, что в state придет pageData
  }
}

// и кобинируем

var reducer = combineReducers({
  userData: userDataReducer,
  pageData: pageDataReducer
});

и так можно с любым, сколько угодно вложенным куском стейта

пример:

const reducer = combineReducers({
  main: combineReducers({
    calculator,
    dashboard
  }),
  user: combineReducers({
    main: user,
    auth,
    registration,
    settings: userSettings,
    recovery
  }),
  navigation,
  drawer,
  searchScreen,
  splashScreen,
  settings
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment