Redux – Basic
- Redux creates a global state which you can share between React components—no parent / child relationship required
- When using Redux, you don’t actually change the state, you create updated copies of the state that are then inserted into your React components.
Store
- Store is the central location in which we store the state
- Store is as a global JavaScript object in which all of our components can access
- This JavaScript object is immutable, meaning it cannot be changed directly, but it can be cloned and replaced with its properties updated instead
- Create a store
// Create a store import { createStore } from 'redux' const store = createStore() export default store
- Store APIs
store.getState() // for accessing the current state of the application store.dispatch() // for changing state via an action store.subscribe() // for responding to state changes
Reducers
- A reducer is a function having two arguments and returns a new state (your app’s current state)
// Set your app's current state before your reducer const initialState = { posts: [], signUpModal: { open: false } } // Then set it as the argument's default value: const reducer = (state = initialState, action) => { return state }
- Integrate reducer to a store
import { createStore } from 'redux' const initialState = { posts: [], signUpModal: { open: false } } const reducer = (state = initialState, action) => { return state } const store = createStore(reducer) export default store
- Reducer is called when
- store is initialized
- actions are dispatched
Actions
- Actions are source of information for the store to be updated
- Actions are JavaScript objects that tell store the type of action to be performed
- Reducers update store based on the value of the
action.type
- Actions contain the
action.payload
for changes to store
- Reducers update store based on the value of the
const action = { type: 'ADD_POST', payload: { id: 1, title: 'Redux Tutorial 2019' } }
- To “call” this action, we need to use our store’s dispatch
store.dispatch({ type: 'ADD_POST', payload: { id: 1, title: 'Redux Tutorial 2019' } })
- When called, this will run the reducer function, and we can then determine how we want to update our state (store)
// INCORRECT!! import { createStore } from 'redux' const initialState = { posts: [], loginModal: { open: false } } const reducer = (state = initialState, action) => { if (action.type === 'ADD_POST') { state.posts.push(action.payload) } return state } const store = createStore(reducer) export default store
This would successfully update our store, but we’re updating our state directly. Redux state is immutable. Remember, we want to create an updated clone so we can view exactly what our app looks like at certain states after actions are dispatched
State can’t be directly changed, to create or update state, we can use the JavaScript spread operator to make sure we don’t change the value of the state directly but instead to return a new object that contains a state passed to it and the payload of the user.
import { createStore } from 'redux' const initialState = { posts: [], loginModal: { open: false } } // Create reducer with action const reducer = (state = initialState, action) => { if (action.type === 'ADD_POST') { return { ...state, posts: [...state.posts, action.payload] } } return state } // Create store with reducer const store = createStore(reducer) export default store
Redux saga toolkit
How to configure a store?
- Configure ReduxInjectors
- This is to dynamically load redux reducers and redux-saga as needed, instead of loading them all upfront
How to init/set/extract data to/ from store?
Init state, action, key in a separate file
slice.js
import { createSlice } from '@reduxjs/toolkit'; export const initialState = { image: null, }; const LightBoxSlice = createSlice({ name: 'light-box', initialState, reducers: { displayImage(state, action) { const { url } = action.payload; state.image = url; }, closeImage(state) { state.image = null; }, }, }); export const { displayImage, closeImage } = LightBoxSlice.actions; export const { name, reducer } = LightBoxSlice;
Set data to store
import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { displayImage } from 'slice.js'; const PhotoFinish = ({ image }) => { // Set data to store const dispatch = useDispatch(); const openImage = useCallback(() => { dispatch(displayImage({ url: image, })); }, [image]); return ( <div> <div onClick={openImage}> <Icon type="search" /> </div> </div> ); }; export default PhotoFinish;
Extract data from store
import React, { useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { useInjectReducer } from 'redux-injectors'; import { closeImage, name, reducer } from './slice'; import { getLightBoxImage } from './selectors'; const ModalLightBox = ({ ...props }) => { // Load reducers useInjectReducer({ name, reducer }); // Extract data from store const imageUrl = useSelector( (state) => state[name]?.imageUrl ); // Set data to store const dispatch = useDispatch(); const close = useCallback(() => { dispatch(closeImage()); }, [dispatch]); return ( <Modal isOpen={image !== null} onRequestClose={close} {...props} > <div> <img src={image} /> <div onClick={close}> <Icon type="close-circle" /> </div> </div> </Modal> ); }; export default ModalLightBox;
How to init/request/set/extract API data to/ from store?
Init state, action, key in a separate file
slice.js
import { createSlice } from '@reduxjs/toolkit'; const initialState = { userInformation: null, }; const MyTestSlice = createSlice({ name: 'my-test', initialState, reducers: { loadUsrInfo(state) { state.userInformation = null; }, storeUsrInfo(state, action) { const { userInformation } = action.payload; state.userInformation = userInformation; }, }, }); export const { loadUsrInfo, storeUsrInfo } = MyTestSlice.actions; export const { name, reducer } = MyTestSlice;
Dispatch action to request API
import React, { useEffect } from 'react'; import { useSelector } from 'react-redux'; import { name, loadUsrInfo } from '../slice'; // Utils import { getUsrInfo } from 'utils/selectors'; import { useSagaRequest } from 'utils/sagaRequest'; const TestComponent = () => { const sagaRequest = useSagaRequest(); useEffect(() => { // Request API sagaRequest.dispatch( loadUsrInfo(), ); }, []); // Get API response const usrInfo = useSelector((state) => state[name]?.userInformation); return ( <Container> {usrInfo && <UsrInfo name={usrInfo?.name} age={usrInfo?.age} sex={usrInfo?.sex} />} </Container> ); }; export default TestComponent;
Request API from Middleware listener
import { put, call } from 'redux-saga/effects'; import { takeEverySagaRequest } from 'utils/sagaRequest'; import { query } from 'utils/apollo'; import { format, addDays } from 'date-fns'; import { simulateDelay } from 'utils/saga'; import { loadUsrInfo, storeUsrInfo } from './slice'; export function* loadUsrInfoHandler() { const { data } = yield call(`https://abc.xyz/user/info`); yield simulateDelay(); yield put( storeUsrInfo({ youMayAlsoLike: data, }), ); return data; } export default function* testSaga() { yield takeEverySagaRequest(loadUsrInfo.type, loadUsrInfoHandler); }
this saga is injected to Middleware like this
import React, { useEffect, useState } from 'react'; import { useInjectReducer, useInjectSaga } from 'redux-injectors'; ... const TestPage = () => { useInjectReducer({ key: name, reducer }); useInjectSaga({ key: name, saga }); ... return ... }; export default TestPage;
Redux saga APIs
yield takeEvery()
Use takeEvery to start a new fetchUser task on each dispatched USER_REQUESTED action:
import { takeEvery } from `redux-saga/effects` function* fetchUser(action) { ... } function* watchFetchUser() { yield takeEvery('USER_REQUESTED', fetchUser) }
yield fork()
function watchStorageEvent(key) { return eventChannel((emitter) => { const handler = (e) => { if (e.key === key) { emitter({ newValue: e.newValue, oldValue: e.oldValue }); } }; window.addEventListener('storage', handler); // The subscriber must return an unsubscribe function return () => { window.removeEventListener('storage', handler); }; }); }
export function* syncNotification() { const channel = yield call(watchStorageEvent, 'notification-items'); try { const { newValue } = yield take(channel); const { feeds, lastUpdated, lastRead } = JSON.parse(newValue); ... } finally { if (yield cancelled()) { channel.close(); } } }
export default function* saga() { yield fork(syncNotification); }
Leave a Reply