React – How to manage state by Redux (1)?

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

Be the first to comment

Leave a Reply

Your email address will not be published.


*