# Redux recalls

# Understand redux store (learnt from Dan)

// mock a redux store
const mockReduxStore = (reducer) => {
  let state = { counters: 0 }, listeners = [];

  const getState = () => state; // return current latest states
  const dispatch = (action) => {
    state.counters = reducer(state.counters, action); // dispatch send new data to reducer function
    listeners.forEach(listener => listener()); // when states updated, we need to notify every listener we have new state updated !!!!, so we call each of listener and tell them a new state updated result !!
    // console.log('counters value? ', state.counters);
  }

  const subscribe = (listener) => {
    listeners.push(listener); // add new listener into listeners array
    return () => {
      listeners = listeners.filter(l => l !== listener); // remove listener to current listeners array (also called: unsubscribe listener)
    }
  }
  // listeners are used to tracking changes, when changes are requested by dispatch function, list

  dispatch({}); // we want to make initial state populated !!!!

  return { getState, dispatch, subscribe };
};

var store = mockReduxStore(counterReducer);

export default store;
// reference: https://egghead.io/lessons/react-redux-store-methods-getstate-dispatch-and-subscribe

# For simple Redux flow recall

Please check codebase react-redux-typescipt ~~ (For year 2021)

# How to implement Redux Saga (Basic workflow for recall only !!)

// Step 1: create store and connect to the app
// middleware initial
const sagaMiddleware = createSagaMiddleware();
// store
const store = compose(
  applyMiddleware(sagaMiddleware),
  window.devToolsExtension && window.devToolsExtension(),
)(createStore)(rootReducer);
// run sagas
sagaMiddleware.run(rootSaga);

// Step 2: Write Saga functions (API caller + Saga caller)
function getUsersAPICall() {
  return fetch(apiUrl, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json'
    },
  })
    .then(res => res.json())
    .catch(err => { throw err });
}

function* fetchUsers() {
  try {
    const users = yield call(getUsersAPICall);
    yield put({ type: GET_USERS_SUCCESS, users });
  } catch (error) {
    yield put({ type: GET_USERS_FAILED, message: error.message });
  }
}

export default function* usersSaga() {
  yield takeEvery(GET_USERS_REQUESTED, fetchUsers);
}

// Step 3: write reducer function
const initialState = {
  users: [],
  loading: false,
  error: null,
};

export default function users(state = initialState, action) {
  switch (action.type) {
    case GET_USERS_REQUESTED:
      return {
        ...state,
        loading: true,
      };
    case GET_USERS_FAILED:
      return {
        ...state,
        loading: false,
        error: action.message
      }
    case GET_USERS_SUCCESS:
      return {
        ...state,
        loading: false,
        users: action.users
      }
    default:
      return state;
  }
};

// Step 4: write action function

export function getUsersAction(users) {
  return {
    type: GET_USERS_REQUESTED,
    payload: users,
  }
};

// Step 5: Implement for UI component

export default function User() {
  const dispatch = useDispatch();
  const users = useSelector(state => state.users.users);
  const loading = useSelector(state => state.users.loading);
  const error = useSelector(state => state.users.error);

  useEffect(() => {
    dispatch(getUsersAction());
  }, []);

  return (
    <div>
      {loading && <p>Loading ...</p>}
      {error && !loading && <p>{error}</p>}
      {users && users.length > 0 && !loading && users.map(
        (user, index) => <Card key={`user-${index}`} user={user} />)}
      {users && !loading && users.length === 0 && <p>No users yet ..</p>}
    </div>
  )
}

// For the complete code details, please check codebase: "redux-redux-saga-recall-2021"

# Redux-Thunk workflow example

// store setup

export const store = createStore(
  combineReducers({
    rates: ratesReducer,
  }),
  applyMiddleware(thunk),
);

// reducers

const initialState = {
  amount: '10',
  currencyCode: 'USD',
  currencyData: { USD: 1.0 },
};

export function ratesReducer(state = initialState, action) {
  switch(action.type) {
    case RATES_AMOUNT_CHANGED:
      return {...state, amount: action.payload};
    case RATES_CURRENCY_CHANGED:
      return {...state, currencyCode: action.payload};
    case RATES_RECEIVED:
      return {...state, currencyData: action.payload};
    default:
      return state;
  }
}

// selector functions (access redux state easily)
export const getAmount = state => state.rates.amount;
export const getCurrencyCode = state => state.rates.currencyCode;
export const getCurrencyData = state => state.rates.currencyData;

// actions

export function changeCurrencyCode(currencyCode) {
  return function changeCurrencyCodeThunk(dispatch) {
    dispatch({
      type: RATES_CURRENCY_CHANGED,
      payload: currencyCode
    }); // thunk action creator

    getExchangeRates(currencyCode, supportedCurrencies) // api call
      .then(rates => {
        dispatch({ // then action creator get payload
          type: RATES_RECEIVED,
          payload: rates
        });
      });
  }
};

// thunks

export function getInitialRates(dispatch, getState) {
  const state = getState();
  const currencyCode = getCurrencyCode(state);
  dispatch(changeCurrencyCode(currencyCode)); // fetch currency data before component get loaded ..
};

# Redux-toolkit

This is currently the modern way of manage the react states, features I discovered so far,

  • Seems like actions and reducers are in the SAME file
import { createSlice, PayloadAction } from '@reduxjs/toolkit'

export interface CounterState {
  value: number
}

const initialState: CounterState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the Immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action: PayloadAction<number>) => {
      state.value += action.payload
    },
  },
})

// Action creators are generated for each case reducer function
export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer
  • all in one line
import { configureStore } from '@reduxjs/toolkit';
// automatically imported redux-devtools-extension
// automatically imported redux thunk middleware
  • RTK query: Elegant way to handle async fetch calls
// For functional file:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import { Pokemon } from './types'

// Define a service using a base URL and expected endpoints
export const pokemonApi = createApi({
  reducerPath: 'pokemonApi',
  baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
  endpoints: (builder) => ({
    getPokemonByName: builder.query<Pokemon, string>({
      query: (name) => `pokemon/${name}`,
    }),
  }),
})

// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi
// For store:
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'

export const store = configureStore({
  reducer: {
    // Add the generated reducer as a specific top-level slice
    [pokemonApi.reducerPath]: pokemonApi.reducer,
  },
  // Adding the api middleware enables caching, invalidation, polling,
  // and other useful features of `rtk-query`.
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(pokemonApi.middleware),
})

// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)

# Why we need to use selector functions for Redux state?

Keep your store state minimal and derived data from the state needed, pick the needed state for component

const mainState = (state: State) => state;
const selectedState = createSelector(mainState, (state) => state.specificProp);