Svelte + Redux + Redux-saga

от автора

Попытка жалкого подобия на хуки useSelector, useDispatch, как в react-redux.

Большинство из нас сталкивались с redux, а те, кто использовал его в ReactJS могли пощупать хуки useSelector, useDispatch, в ином случае через mstp, mdtp + HOC connect. А что со svelte? Можно навернуть, или найти что-то похожее на connect, по типу svelte-redux-connect, описывать огромные конструкции, которые будем отдавать в тот самый connect:

const mapStateToProps = state => ({   users: state.users,   filters: state.filters });  const mapDispatchToProps = dispatch => ({   addUser: (name) => dispatch({     type: 'ADD_USER',     payload: { name }   }),   setFilter: (filter) => dispatch({     type: 'SET_FILTER',     payload: { filter }   })  });

Прямо какие-то страшные флэшбэки до середины 2018, до введения хуков :). Хочу хуки в svelte. Что мы можем из него взять? Хм… store у svelte глобальный, не нужны никакие провайдеры с контекстом (шучу, нужны для разделения контекстов, но пока выкинем). Значит так: мы создаем redux-store, потом попробуем написать наши жалкие хуки для удобства использования.

Итак, наши константы:

//constants.js export const GET_USER = '@@user/get' export const FETCHING_USER = '@@user/fetch' export const SET_USER = '@@user/set'

Редюсер:

//user.js import {FETCHING_USER, SET_USER} from "./constants";  const initialState = {   user: null,   isFetching: false }  export default function user(state = initialState, action = {}){   switch (action.type){     case FETCHING_USER:     case SET_USER:       return {         ...state,         ...action.payload       }     default:       return state   } }

Экшены:

//actions.js import {FETCHING_USER, GET_USER, SET_USER} from "./constants";  export const getUser = () => ({   type: GET_USER })  export const setUser = (user) => ({   type: SET_USER,   payload: {     user   } })  export const setIsFetchingUser = (isFetching) => ({   type: FETCHING_USER,   payload: {     isFetching   } })

Селекторы. К ним вернемся отдельно:

//selectors.js import {createSelector} from "reselect"; import path from 'ramda/src/path'  export const selectUser = createSelector(   path(['user', 'user']),   user => user )  export const selectIsFetchingUser = createSelector(   path(['user', 'isFetching']),   isFetching => isFetching )

И главный combineReducers:

//rootReducer.js import {combineReducers} from "redux"; import user from "./user/user";  export const reducers = combineReducers({   user })

Теперь надо прикрутить redux-saga, а в качестве api у нас будет https://randomuser.me/api/. Во время тестирования всего процесса, эта апи очень быстро работала, а я очень сильно хотел посмотреть на лоадер подольше (у каждого свой мазохизм), поэтому я завернул таймаут в промис на 3 сек.

//saga.js import {takeLatest, put, call, cancelled} from 'redux-saga/effects' import {GET_USER} from "./constants"; import {setIsFetchingUser, setUser} from "./actions"; import axios from "axios";  const timeout = () => new Promise(resolve => {   setTimeout(()=>{     resolve()   }, 3000) })  function* getUser(){   const cancelToken = axios.CancelToken.source()   try{     yield put(setIsFetchingUser(true))     const response = yield call(axios.get, 'https://randomuser.me/api/', {cancelToken: cancelToken.token})     yield call(timeout)     yield put(setUser(response.data.results[0]))     yield put(setIsFetchingUser(false))   }catch (error){     console.error(error)   }finally {     if(yield cancelled()){       cancelToken.cancel('cancel fetching user')     }     yield put(setIsFetchingUser(false))   } }  export default function* userSaga(){   yield takeLatest(GET_USER, getUser) }
//rootSaga.js import {all} from 'redux-saga/effects' import userSaga from "./user/saga";  export default function* rootSaga(){   yield all([userSaga()]) }

И наконец инициализация store:

//store.js import {applyMiddleware, createStore} from "redux"; import {reducers} from "./rootReducer"; import {composeWithDevTools} from 'redux-devtools-extension'; import {writable} from "svelte/store";  import createSagaMiddleware from 'redux-saga'; import rootSaga from "./rootSaga";  const sagaMiddleware = createSagaMiddleware()  const middleware = applyMiddleware(sagaMiddleware)  const store = createStore(reducers, composeWithDevTools(middleware))  sagaMiddleware.run(rootSaga)  // берем изначальное состояние из store const initialState = store.getState()  // написали writable store для useSelector export const useSelector = writable((selector)=>selector(initialState))  // написали writable store для useDispatch, хотя можно было и без этого // но для симметрии использования оставил так export const useDispatch = writable(() => store.dispatch)  // подписываемся на обновление store store.subscribe(()=>{   const state = store.getState()   // при обновлении store обновляем useSelector, тут нет никакой мемоизации,    // проверки стейтов, обработки ошибок и прочего очень важного для оптимизации   useSelector.set(selector => selector(state)) })

Всё. Самое интересное начинается с 18 строки. После того, как приходит понятие того, что мы написали, возникает вопрос — если я буду использовать useSelector в 3 разных компонентах с разными данными из store — у меня будут обновляться все компоненты сразу? Нет, обновятся и перерисуются данные, которые мы используем. Даже если логически предположить, что при каждом чихе в store у нас меняется ссылка на функцию, то и обновление компонента по идее должно быть, но его нет. Я честно не до конца разобрался как это работает, но я доберусь до сути, не ругайтесь 🙂

Хуки готовы, как использовать?

Начнем c useDispatch. Его вообще можно было не заворачивать в svelte-store и сделать просто
export const useDispatch = () => store.dispatch, только по итогу с useSelector мы используем store bindings, а с useDispatch нет — сорян, всё же во мне есть частичка маленького перфекционизма. Используем хук useDispatch в App.svelte:

<!--App.svelte--> <script>   import {getUser} from "./store/user/actions";   import {useDispatch} from "./store/store";   import Loader from "./Loader.svelte";   import User from "./User.svelte";   // создаем диспатчер   const dispatch = $useDispatch()   const handleClick = () => {     // тригерим экшен     dispatch(getUser())   } </script>  <style>     .wrapper {         display: inline-block;         padding: 20px;     }     .button {         padding: 10px;         margin: 20px 0;         border: none;         background: #1d7373;         color: #fff;         border-radius: 8px;         outline: none;         cursor: pointer;     }     .heading {         line-height: 20px;         font-size: 20px;     } </style>  <div class="wrapper">     <h1 class="heading">Random user</h1>     <button class="button" on:click={handleClick}>Fetch user</button>     <Loader/>     <User/> </div>
Кнопока которая тригерит экшен
Кнопока которая тригерит экшен

Вот такая вот загогулина у меня свёрстана. При нажатии на кнопку Fetch user, тригерим экшен GET_USER. Смотрим в Redux-dev-tools — экшен вызвался, всё хорошо. Смотрим network — запрос к апи выполнен, тоже всё хорошо:

Теперь нужно показать процесс загрузки и полученного нами пользователя. Используем useSelector:

<!--Loader.svelte--> <script>     import {useSelector} from "./store/store";     import {selectIsFetchingUser} from "./store/user/selector"; 		// Только в такой конструкции мы можем получить из store данные,      // выглядит не так страшно и не лагает, я проверял :3     $: isFetchingUser = $useSelector(selectIsFetchingUser) </script>  <style>     @keyframes loading {         0% {             background: #000;             color: #fff;         }         100% {             background: #fff;             color: #000;         }     }     .loader {         background: #fff;         box-shadow: 0px 0px 7px rgba(0,0,0,0.3);         padding: 10px;         border-radius: 8px;         transition: color 0.3s ease-in-out, background 0.3s ease-in-out;         animation: loading 3s ease-in-out forwards;     } </style>  {#if isFetchingUser}     <div class="loader">Loading...</div> {/if}

Лоадер рисуется. Данные из store прилетают, теперь надо показать юзера:

<!--User.svelte--> <script>     import {useSelector} from "./store/store";     import {selectIsFetchingUser,selectUser} from "./store/user/selector";      $: user = $useSelector(selectUser)     $: isFetchingUser = $useSelector(selectIsFetchingUser) </script> <style>     .user {         background: #fff;         box-shadow: 0px 0px 7px rgba(0,0,0,0.3);         display: grid;         padding: 20px;         justify-content: center;         align-items: center;         border-radius: 8px;     }     .user-image {         width: 100px;         height: 100px;         background-position: center;         background-size: contain;         border-radius: 50%;         margin-bottom: 20px;         justify-self: center;     } </style> {#if user && !isFetchingUser}     <div class="user">         <div class="user-image" style={`background-image: url(${user.picture.large});`}></div>         <div>{user.name.title}. {user.name.first} {user.name.last}</div>     </div> {/if}

Пользователя так же получили.

Итог

Запилили какие-никакие подобия на хуки, вроде удобно, но не известно как это отразится в будущем, если сделать из этого mini-app на пару страниц. Саги так же пашут. Через redux devtools можно дебажить redux и прыгать от экшена к экшену, всё хорошо работает.

ссылка на оригинал статьи https://habr.com/ru/post/541188/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *