Meteor Subscriptions & React useState/useEffect

I’m using a custom React hook for forms. The high level overview:

const component = () => {
  // load subscription
  // set state variable once data has loaded from server-publish
  // pass this init state into a custom hook:
  const {inputs, handleInputChange, handleSubmit} = useForm(signup, initialState);
  // return form
  return (<form><inputs .../></form>)
}

At a high level the custom hook:

const useForm = (callback, initialState) => {
  // set state of inputs
  const [inputs, setInputs] = useState(initialState);

  const handleSubmit = event => {
    // code removed...
  };
  
  const handleInputChange = event => {
  // code removed...
  };
  
  return {
    handleSubmit,
    handleInputChange,
    inputs,
  };
}
export default useForm;

The issue I’m having is, the useState gets initialized with {} because the server hasn’t returned data by the time this hook is called, and therefore the form always starts out with no data even in the case where their’s data for the user.

If I use a useEffect inside the form hook:


  useEffect(() => {
    if (initialState) {
      setInputs(() => ({...initialState}));
    }
   }, [initialState]);

It does in fact add the state from the server to the form as desired, but constantly reruns, even when it seems initalState is has not changed.

How can we init values to a hook when we don’t have the data from the server?

or more generically…

How does one set the initial state of a useState hook only the first time said initial state is not null (in my case, when the Meteor Publication has time to send the data to the client)?

Wrap the component in React.memo() so that it only re-renders when its props have changed. useEffect() does not obsolete React.memo().

Also, React.memo() is for turning any component into the equivalent of React.PureComponent, whether it’s a class component or a function component - only re-render if props have changed meaningfully.

If you wrap it in React.memo() then it will only render when the props change (after you call setInputs in this case).

Thanks @martineboh

When I wrap my component inside a memo I get:

TypeError: useForm is not a function

Was this:

const useForm = (callback, initialState) => {
  // removed code ...
}

Now it looks like this:

const useForm = React.memo((callback, initialState) => {
 // code removed...
});

Calling component:

  const {inputs, handleInputChange, handleSubmit} = useForm(signup, initialState);

Syntax correct: :see_no_evil:

const useForm = React.memo(function useForm(props) {
  /* render using props */
});

Then do this:

function useForm(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /*
Basically, this function is shouldComponentUpdate, except you return true if you want it to not render.
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(useForm, areEqual);

useEffect only runs when the component is fully rendered.

Again:

Using setState inside useEffect will create an infinite loop that most likely you don’t want to cause.

useEffect is called after each render and when setState is used inside of it, it will cause the component to re-render which will call useEffect and so on and so on.

The only case that using useState inside of useEffect will not cause an infinite loop is when you pass an empty array as a second argument to useEffect like useEffect(() => {....}, []) which means that the effect function should be called once: after the first mount/render only.

Thanks!

But that’s the thing, I passed in initialState (which is the state of the form, that is sent to the client via a Publication), and even when it was the same useEffect still reran:

// the publication returns and populates the intialState values

  // ...
  if (loading) {
    console.log(`inside Signup loading...`)
  } else {
    const person = Person.findOne({ userId: userId }) || '';
    if (person) {
      initialState.firstName = person.firstName || '', 
      initialState.lastName = person.lastName || '', 
      initialState.email = person.email || '', 
      initialState.password = person.password || '' 
    }
  }
  // ...
  const {inputs, handleInputChange, handleSubmit} = useForm(signup, initialState);

// the inital state will have values after the publication returns with data from the database

const useForm = (callback, initialState) => {
  const [inputs, setInputs] = useState(initialState);
  // ...
  // reruns even when it's the same value for initalState
  useEffect(() => {
    if (initialState) {
      setInputs(() => ({...initialState}));
    }
  }, [initialState]);
  // ...

Try calling the useForm like this:

function useForm(props) {
  /* render using props */
}
const areEqual = (prevProps, nextProps) => true;

export default React.memo(useForm, areEqual);

Or:

const useForm = React.memo(useForm(props) => {
  return /*whatever jsx you like */
}, areEqual);
export default useForm

Thanks!

// Here I wrapped the two functions in a React.memo, but I need the returns from the hook so I did it like this:

// ...

  const areEqual = (prevProps, nextProps) => {
    return (
      prevProps.firstName === nextProps.firstName &&
      prevProps.lastName === nextProps.lastName &&
      prevProps.email === nextProps.email &&
      prevProps.password === nextProps.password
    )
  };

  const {inputs, handleInputChange, handleSubmit} = React.memo(useForm(signup, initialState), areEqual);

// ...

// Didn’t think I needed to change the signature of the function since I’m wrapping in memo in the calling module, but maybe I do.

// ...

const useForm = (callback, initialState) => {

// ...

This structure didn’t work. But also the sturucture you laid out doesn’t work because I need to return values from the useForm and your examples don’t provide for that. Also, OK React.memo keeps the function running once and does a difference check, but useEffect is suppose to do that as well – and doesn’t for some reason based on the initalState.

For the comparison of props you can use a deep compare like this: https://github.com/FormidableLabs/react-fast-compare.
" How does one set the initial state of a useState hook only the first time" . Hooks are stateless. So I think the question is more like what should you present when you have no data (yet)? Input placeholders? A spinner? A loading bar. However, you have props so maybe you can initialize with a props object (from Redux if you use or from a parent component) and then switch props.

I just solved it like this (seems like a hack).

  // ...

  let initialState = {};
  let stateSet = false; // <= set the state to false, the useEffect will run as many times as needed
  
  if (loading) {
    console.log(`inside Signup loading...`)
  } else {
    const person = Person.findOne({ userId: userId }) || '';
    if (person) {
      initialState.firstName = person.firstName || ''; 
      initialState.lastName = person.lastName || ''; 
      initialState.email = person.email || '';
      initialState.password = person.password || '';
      stateSet = true; // <= important, after the subscription is filled, I set the state
    }
  }

// ...

// pass in the new boolean here
  const {inputs, handleInputChange, handleSubmit} = useForm(signup, initialState, stateSet);

// I now pass in the setState like so
const useForm = (callback, initialState, stateSet, validate) => {
  const [inputs, setInputs] = useState(initialState);

// ...

 // turn "OFF" useEffect with the boolean
  useEffect(() => {
    if (initialState) {
      setInputs(() => ({...initialState}));
    }
  }, [stateSet]);

// ...

Again, I don’t like this, @paulishca I’ll look at deep compare of initalState instead of the boolean approach now. Could it be because initialState was not stored somewhere in the function?

Another discussion to use as reference.