Chatmosphere / chatmosphere-app

We are a small group of friendly humans working on an open source project to make informal video calls more fun and dynamic.
https://chatmosphere.cc/
Other
161 stars 40 forks source link

Evaluate use of one Store with reducers -> testable #7

Open dkgrieshammer opened 3 years ago

dkgrieshammer commented 3 years ago

One Option as suggested by zustand staff out of their experience was using one store in the end (makes picking way easier)

I think with using reducers this could work since the store object is not bloated with all method details; also the methods itself would be pure and thus testable

mandymozart commented 3 years ago

Maybe like this:

ActionMap Type Definition

/**
 * Create an action map for reducers.
 * We check which action is used and dynamically generate the types for the payload
 * Courtesy of: https://medium.com/hackernoon/finally-the-typescript-redux-hooks-events-blog-you-were-looking-for-c4663d823b01
 */
type ActionMap<M extends { [index: string]: any }> = {
  [Key in keyof M]: M[Key] extends undefined
    ? {
        type: Key;
      }
    : {
        type: Key;
        payload: M[Key];
      };
};

export default ActionMap;

GlobalActions

type GlobalActions = ActionMap<ActionPayload>[keyof ActionMap<ActionPayload>];

Actions as enums

enum Action {
  addUser = "ADD_USER",
}

ActionPayload Mapping

type ActionPayload = {
  [Action.addUser]: AddUserParam;
}

Params

type AddUserParam = {
  id: string;
 name: string;
};

Initial State

const initialState: ConferenceState = {
  users: [],
  // yes, localUser in here would work nicely
};

Reducer

const reducer = (state: ConferenceState, action: GlobalActions): ConferenceState => {
  switch (action.type) {
    case Action.addUser:
      return addUser(state, action.payload);
   // ...
    default:
      return state;
  }
};

Methods (can live inside their own files)

function addUser(
  state: ConferenceState,
  { id, name }: AddUserParam
): ConferenceState {
  let newUsers: User[];

    newUsers = updateArray(state.users, userIndex, {
      ...state.users[userIndex],
      name: name,
    });

  return {
    ...state,
    users: newUsers,
  };
}

Context

const ConferenceContext = createContext<{
  conference: ConferenceState;
  dispatch: React.Dispatch<GlobalActions>;
}>({ conference: initialState, dispatch: () => [] });

export const ConferenceStoreProvider = ({
  children,
}: {
  children: JSX.Element;
}) => {
  const [store, dispatch] = useReducer(reducer, initialState);

  return (
    <ConferenceContext.Provider value={{ conference: store, dispatch }}>
      {children}
    </ConferenceContext.Provider>
  );
};

Actual store

export const useConference = () => useContext(ConferenceContext);

export const useConferenceStore = () => {
  const { conference, dispatch } = useConference();
  return {
    users: conference.users,
    addUser: (id: string, name: string) =>
      dispatch({ type: Action.addUser, payload: { id, name } }),
   //  ex: localUser, .... etc. 
  };
};

Utility (updateArray)

function updateArray<T>(array: T[], index: number, value: T): T[] {
  const newArray = [...array];
  newArray.splice(index, 1, value);
  return newArray;
}