Skip to content

Instantly share code, notes, and snippets.

@codecorsair
Created January 6, 2020 19:38
Show Gist options
  • Save codecorsair/98e6596d22a7f4a07ec6f6afd1a0001e to your computer and use it in GitHub Desktop.
Save codecorsair/98e6596d22a7f4a07ec6f6afd1a0001e to your computer and use it in GitHub Desktop.
Simple React shared state using hooks.
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import React, { useState, useEffect } from 'react';
interface StateDef<TState> {
key: string;
state: TState;
setters: React.Dispatch<React.SetStateAction<TState>>[];
reducer: (currentState: TState, action: { type: string }) => TState;
}
const sharedStates: {[key: string]: StateDef<any>} = {};
export function createSharedState<TState>(
key: string,
initialState: TState,
resetState?: (currentState: TState) => TState): () => [TState, (state: TState) => TState, () => void] {
if (sharedStates[key]) {
console.warn(`Attempted to create a new shared state with key ${key} when a state with this key already exits.`);
return;
}
sharedStates[key] = {
key,
state: initialState,
setters: [],
reducer: null,
};
return function() {
const def = sharedStates[key] as StateDef<TState>;
if (!def) {
throw `Failed to find shared state with key ${key} when using created State method.`;
}
const [state, set] = useState(def.state);
useEffect(() => () => {
def.setters = def.setters.filter(setter => setter !== set);
}, []);
if (!def.setters.includes(set)) {
def.setters.push(set);
}
function setState(newState: TState) {
def.state = newState;
def.setters.forEach(setter => setter(def.state));
return def.state;
}
function reset() {
if (resetState) {
setState(resetState(def.state));
}
}
return [state, setState, reset];
};
}
export function createSharedStateWithReducer<TState, TActions extends { type: string }>(
key: string,
initialState: TState,
reducer: (currentState: TState, actions: TActions) => TState,
resetState?: (currentState: TState) => TState): () => [TState, React.Dispatch<TActions>, () => void] {
if (sharedStates[key]) {
console.warn(`Attempted to create a new shared state with key ${key} when a state with this key already exits.`);
return;
}
sharedStates[key] = {
key,
state: initialState,
setters: [],
reducer,
};
return function() {
const def = sharedStates[key] as StateDef<TState>;
if (!def) {
throw `Failed to find shared state with key ${key} when using created State method.`;
}
const [state, set] = useState(def.state);
useEffect(() => () => {
def.setters = def.setters.filter(setter => setter !== set);
}, []);
if (!def.setters.includes(set)) {
def.setters.push(set);
}
function dispatch(action: TActions) {
def.state = def.reducer(def.state, action);
def.setters.forEach(setter => setter(def.state));
}
function reset() {
if (resetState) {
def.state = resetState(def.state);
def.setters.forEach(setter => setter(def.state));
}
}
return [state, dispatch, reset];
};
}
@codecorsair
Copy link
Author

codecorsair commented Jan 6, 2020

A usage example:

export enum Routes {
  Main,
  Magic,
  Melee,
  Archery,
  Throwing,
  Shout,
  Song,
  Misc,
  Components,
}

export interface State {
  activeRoute: Routes;
}

const defaultAbilityBookState: State = {
  activeRoute: Routes.Components,
};

export const useAbilityBookReducer
  = createSharedStateWithReducer('ability-book-state', defaultAbilityBookState, abilityBookReducer);

interface SetActiveRoute {
  type: 'set-active-route';
  activeRoute: Routes;
}

function setActiveRoute(state: State, action: SetActiveRoute) {
  return {
    ...state,
    activeRoute: action.activeRoute,
  };
}

interface Reset {
  type: 'reset';
}

function reset(state: State, action: Reset) {
  return {
    ...defaultAbilityBookState,
  };
}

export type Actions = SetActiveRoute | Reset;
function abilityBookReducer(state: State, action: Actions) {
  switch (action.type) {
    case 'set-active-route': return setActiveRoute(state, action);
    case 'reset': return reset(state, action);
    default: return state;
  }
}

With a component:

export function AbilityBook(props: Props) {
  
  const [state, dispatch] = useAbilityBookReducer();
  
  .. bits omitted ...

  useEffect(() => {
    const listener = game.on('refetch-ability-book', refetch);
    return function() {
      dispatch({ type: 'reset' });
      listener.clear();
    };
  }, []);

  function onRouteClick(route: Routes) {
    dispatch({ type: 'set-active-route', activeRoute: route });
  }

  ... bits omitted ...

return (
    <GraphQL
      query={{
        query,
        variables: {
          class: Archetype[camelotunchained.game.selfPlayerState.classID],
        },
      }}
      onQueryResult={handleQueryResult}>
      {(graphql: GraphQLResult<AbilityBookQuery.Query>) => {
        return (
          <AbilityBookContext.Provider
            value={{
              loading,
              refetch: graphql.refetch,
              abilityNetworks,
              abilityNetworkToAbilities,
              abilityComponents,
              abilityComponentIDToProgression,
              componentCategoryToComponentIDs,
            }}>
            <Container>
              <Header title={getHeaderTitle()} />
              <Content>
                <SideNav
                  activeRoute={state.activeRoute}
                  onRouteClick={onRouteClick}
                  abilityNetworkNames={Object.keys(abilityNetworks)}
                />
                <PageContainer>
                  {!loading && renderSelectedPage()}
                </PageContainer>
              </Content>
            </Container>
          </AbilityBookContext.Provider>
        );
      }}
    </GraphQL>
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment