Svelte + Redux + Redux-saga

Un intento de una patética similitud con los ganchos useSelector, useDispatch, como en react-redux.

La mayoría de nosotros nos hemos encontrado con redux, y aquellos que lo usaron en ReactJS podrían sentir los ganchos useSelector, useDispatch, de lo contrario a través de mstp, mdtp + HOC connect. ¿Y esbelto? Puede estropearlo o encontrar algo similar para conectar, como svelte-redux-connect, describir construcciones enormes que enviaremos a la misma conexión:





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 }
  }) 
});
      
      



Solo algunos flashbacks de miedo hasta mediados de 2018, antes de la introducción de ganchos :). Quiero anzuelos esbeltos. ¿Qué podemos sacar de ella? Hmm ... la tienda de svelte es global, no se necesitan proveedores con un contexto (es broma, se necesitan para separar contextos, pero eliminémoslos por ahora). Significa así: creamos una tienda redux, luego intentaremos escribir nuestros patéticos ganchos para facilitar su uso.





Entonces nuestras constantes:





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



Reductor:





//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
  }
}
      
      



Comportamiento:





//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
  }
})
      
      



Selectores. Volvamos a ellos por separado:





//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
)
      
      



Y los principales reductores de la cosechadora:





//rootReducer.js
import {combineReducers} from "redux";
import user from "./user/user";

export const reducers = combineReducers({
  user
})
      
      



Ahora necesitamos adjuntar redux-saga, y como api tendremos https://randomuser.me/api/ . Durante la prueba de todo el proceso, esta API funcionó muy rápido, y realmente quería mirar el cargador por más tiempo (todos tienen su propio masoquismo), así que envolví el tiempo de espera en una promesa de 3 segundos.





//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()])
}
      
      



Finalmente, inicializando la tienda:





//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>
      
      



El botón que activa la acción.

. 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}
      
      



.





Anotamos algunas similitudes con los hooks, parece conveniente, pero no se sabe cómo afectará esto en el futuro, si hacemos una mini-aplicación de esto por un par de páginas. Las sagas también aran. A través de redux devtools, puede depurar redux y pasar de una acción a otra, todo funciona bien.








All Articles