Русский
Русский
English
Статистика
Реклама

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.jsexport const GET_USER = '@@user/get'export const FETCHING_USER = '@@user/fetch'export const SET_USER = '@@user/set'

Редюсер:

//user.jsimport {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.jsimport {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.jsimport {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.jsimport {combineReducers} from "redux";import user from "./user/user";export const reducers = combineReducers({  user})

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

//saga.jsimport {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.jsimport {all} from 'redux-saga/effects'import userSaga from "./user/saga";export default function* rootSaga(){  yield all([userSaga()])}

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

//store.jsimport {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)// берем изначальное состояние из storeconst initialState = store.getState()// написали writable store для useSelectorexport const useSelector = writable((selector)=>selector(initialState))// написали writable store для useDispatch, хотя можно было и без этого// но для симметрии использования оставил такexport const useDispatch = writable(() => store.dispatch)// подписываемся на обновление storestore.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 и прыгать от экшена к экшену, всё хорошо работает.

Источник: habr.com
К списку статей
Опубликовано: 07.02.2021 02:13:24
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Javascript

Sveltejs

Svelte

Redux

Redux-saga

Hooks

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru