2

I have a form composed of several input components. The form data is shared and shareable across these sibling components via a React context and the React hook useContext.

I am stumbling on how to optionally async load data into the same context. For example, if the browser URL is example.com/form, then the form can load with the default values provided by a FormContext (no problem). But if the user is returning to finish a previously-edited form by navigating to example.com/form/:username/:form-id, then application should fetch the data using those two data points. Presumably this must happen within the FormContext somehow, in order to override the default empty form initial value.

  1. Are url params even available to a React.createContext function?
  2. If so, how to handle the optional data fetch requirement when hooks are not to be used with in a conditional?
  3. How to ensure that the data fetch occurs only once?
  4. Lastly, the form also saves current state to local storage in order to persist values on refresh. If the data fetch is implemented, should a refresh load the async data or the local storage? I'm leaning towards local storage because that is more likely to represent what the user last experienced. I'm open to opinions and thoughts on implementation.

FormContext

export const FormContext = React.createContext();
export const FormProvider = props => {
  const defaultFormValues = {
    firstName: "",
    lastName: "",
    whatever: "",
  };
  const [form, setForm] = useLocalStorage(
    "form",
    defaultFormValues
  );
  return (
    <FormContext.Provider value={{ form, setForm }}>
      {props.children}
    </FormContext.Provider>
  );
};

Reference for useLocalStorage

Kwhitejr
  • 2,206
  • 5
  • 29
  • 49
  • 1
    If you're using React Router you can get the query params through the ```match``` prop. Here's a [demo](https://reacttraining.com/react-router/web/example/url-params). You can then use the params to fetch whatever data you need, store it in your state, and pass it conditionally into the ```value``` prop of your Provider. To prevent refetching you could use some form of [usePrevious](https://stackoverflow.com/questions/53446020/how-to-compare-oldvalues-and-newvalues-on-react-hooks-useeffect) functionality to only fetch if the data has changed (wish they made this easier with hooks). – Chris B. Sep 16 '19 at 02:36

2 Answers2

1

I think the answer you're looking for is Redux, not the library but the workflow. I did find it curious React doesn't give more guidance on this. I'm not sure what others are doing but this is what I came up with.

First I make sure the dispatch from useReducer is added to the context. This is the interface for that:

export interface IContextWithDispatch<T> {
  context: T;
  dispatch: Dispatch<IAction>;
}

Then given this context:

export interface IUserContext {
  username: string;
  email: string;
  password: string;
  isLoggedIn: boolean;
}

I can do this:

export const UserContext = createContext<IContextWithDispatch<IUserContext>>({
  context: initialUserContext,
  dispatch: () => {
    return initialUserContext;
  },
});

In my top level component I memoize the context because I only want one instance. This is how I put it all together

import memoize from 'lodash/memoize';
import {
  IAction,
  IContextWithDispatch,
  initialUserContext,
  IUserContext,
} from './domain';

const getCtx = memoize(
  ([context, dispatch]: [IUserContext, React.Dispatch<IAction>]) =>
    ({ context, dispatch } as IContextWithDispatch<IUserContext>),
);
const UserProvider = ({ children }) => {
  const userContext = getCtx(useReducer(userReducer, initialUserContext)) as IContextWithDispatch<
      IUserContext
      >;
  useEffect(() => {
    // api call to fetch user info
  }, []);
  return <UserContext.Provider value={userContext}>{children}</UserContext.Provider>;
};

Your userReducer will be responding to all dispatch calls and can make API calls or call another service to do that etc... The reducer handles all changes to the context. A simple reducer could look like this:

export default (user, action) => {
  switch (action.type) {
    case 'SAVE_USER':
      return {
        ...user,
        isLoggedIn: true,
        data: action.payload,
      }
    case 'LOGOUT':
      return {
        ...user,
        isLoggedIn: false,
        data: {},
      }
    default:
      return user
  }
}

In my components I can now do this:

const { context, dispatch } = useContext<IContextWithDispatch<IUserContext>>(UserContext);

where UserContext gets imported from the export defined above.

In your case, if your route example.com/form/:username/:form-id doesn't have the data it needs it can dispatch an action and listen to the context for the results of that action. Your reducer can make any necessary api calls and your component doesn't need to know anything about it.

Rip Ryness
  • 651
  • 1
  • 7
  • 14
  • So the reducer lives independently from the context, but reducer `state` and `dispatch` are added to the context at initialization? Can you also explain a bit more about why memoization is required here? If the top level component is wrapped in a , how would you get multiple instances of context? – Kwhitejr Oct 03 '19 at 14:51
  • This was something I came up with a couple of months back when I was learning hooks and I have not used it in production. Regarding memoization, I was trying to remember if I had a good reason and I was planning to do some testing to see if it really is necessary. My hunch, as you said, is that it is not necessary. I add `dispatch` but not `state` to the context so I end up with an `IContextWithDispatch` object so there is easy access to the `dispatch` function. – Rip Ryness Oct 03 '19 at 15:31
  • Upon further reflection, memoization is necessary because `userContext` must be created where it, as opposed to inside the `useEffect` call because I'm using it in the `Provider`. Due to the nature of functional components, this gets called on every render cycle. I think when I created this I didn't really understand the intent of `useReducer` being at the `state` level and I'm currently thinking through this. – Rip Ryness Oct 03 '19 at 15:49
  • Here's an overview of what I came up with: https://gist.github.com/kwhitejr/df3082d2a56a00b7b75365110216b395 – Kwhitejr Oct 07 '19 at 03:25
0

Managed to accomplish most of what I wanted. Here is a gist demonstrating the basic ideas of the final product: https://gist.github.com/kwhitejr/df3082d2a56a00b7b75365110216b395

Happy to receive feedback!

Kwhitejr
  • 2,206
  • 5
  • 29
  • 49