[JavaScript, SvelteJS] Svelte + Redux + Redux-saga
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Попытка жалкого подобия на хуки 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 и прыгать от экшена к экшену, всё хорошо работает.
===========
Источник:
habr.com
===========
Похожие новости:
- [Настройка Linux, Разработка веб-сайтов, CSS, JavaScript] Просто вертикальный монитор не значит, что я на телефоне (перевод)
- [JavaScript, Монетизация мобильных приложений, Голосовые интерфейсы] Как создавать навыки для виртуальных ассистентов Салют, выйти на многомиллионную аудиторию Сбера и выиграть 2,5 млн руб?
- [JavaScript, ReactJS] createRef, setRef, useRef и зачем нужен current в ref
- [IT-стандарты, VueJS, TypeScript] Идеальное Vue приложение на Typescript
- [Разработка веб-сайтов, JavaScript] Основы отладки клиентских JS-приложений
- [Разработка веб-сайтов, JavaScript, Проектирование и рефакторинг, ReactJS] Архитектурный паттерн Dependency Injection в React-приложении
- [Тестирование IT-систем, JavaScript] Не используйте фикстуры в Cypress и юнит-тесты — используйте фабричные функции (перевод)
- [Программирование] Дайджест материалов сообщества Deno (01.01 — 31.01)
- [JavaScript, Node.JS, ReactJS] Домашнее IoT-устройство глазами JS-разработчика
- [JavaScript, Программирование, HTML, Браузеры, DIY или Сделай сам] How I create browser applications inside browsers (перевод)
Теги для поиска: #_javascript, #_sveltejs, #_svelte, #_sveltejs, #_redux, #_reduxsaga, #_hooks, #_javascript, #_sveltejs
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:25
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Попытка жалкого подобия на хуки 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 } }) }); //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 ) //rootReducer.js
import {combineReducers} from "redux"; import user from "./user/user"; export const reducers = combineReducers({ user }) //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.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)) }) 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 =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:25
Часовой пояс: UTC + 5