re: What's hard about React Hooks for you? VIEW POST

FULL DISCUSSION
 

References in custom hooks

For me, one the hardest thing with hooks is about following the references when creating custom hooks.

I end up with a lot of useMemo()s and the "not recommended" useEventCallback technique and a lot of ref tracing of make sure it's not my custom hook causing unnecessary renders.

Example:

const useRelatedState = (
  items,
  defaultMapToState = () => {},
  identifier = v => v
) => {
  const [map, setMap] = useState(() => new Map());

  const tuplesWithRelatedState = useMemo(
    () =>
      items.map(item => {
        const id = identifier(item);
        return [item, map.has(id) ? map.get(id) : defaultMapToState(item)];
      }),
    [items, map, defaultMapToState, identifier]
  );

  const setStateForItem = useCallback(
    (item, newState) =>
      setMap(map => {
        const id = identifier(item);
        const currentState = map.has(id)
          ? map.get(id)
          : defaultMapToState(item);
        if (typeof newState === 'function') {
          newState = newState(currentState);
        }
        if (currentState !== newState) {
          return new Map([...map.entries()].concat([[id, newState]]));
        }
        return map;
      }),
    [setMap, identifier]
  );

  const ref = useRef();
  ref.current = useCallback(
    mapRelatedState => {
      const newStates = items.map(item =>
        mapRelatedState(item, map.get(identifier(item)))
      );
      if (
        items.length !== newStates.length ||
        items.some((item, i) => map.get(identifier(item)) !== newStates[i])
      ) {
        setMap(new Map(items.map((item, i) => [item, newStates[i]])));
      }
    },
    [items, map, setMap, identifier]
  );
  const setAllState = useCallback(
    mapRelatedState => {
      ref.current(mapRelatedState);
    },
    [ref]
  );

  return [tuplesWithRelatedState, setStateForItem, setAllState];
};

Async effect without use actions

Also, any async effects that change state but are not caused by a user action, such as loading a list of things when first rendering a component...

const AsyncDataList = () => {
  const [muppets = [], setMuppets] = useState();
  useEffect(() => {
    fetchMuppets().then(setMuppets);
  }, []);

  return (
    <ul>
      {muppets.map(muppet => {
        return <li key={muppet}>{muppet}</li>;
      })}
    </ul>
  );
};

...makes the component annoying to test because it currently throws up a warning about not being wrapped in act and currently act() is a synchronous call

describe('Test a loading component', () => {
  afterEach(cleanup);

  test('async data loading', async () => {
    const { getByText } = render(<AsyncDataList />);
    await wait(() => getByText('Kermit'));
    expect(getByText('Kermit')).toBeInTheDocument();
  });
});
/*
Warning: An update to AsyncDataList inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  * fire events that update state *
});
* assert on the output *

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act
*/

There is going to be a "fix in the next version", about the asynchronous part, but will it help with async effects that have no user action to initiate them?


Reset state based on props

Also I often find myself wishing I could "reset the state" based on a prop change. The only easy work around I've found that seems to work is just dispatching/calling a callback in useEffect that run when the prop changes.

const MyComponent = ({ theProp }) => {
  const [someState, dispatch] = useReducer(reducer, { initialState: true ));

  // ๐Ÿ˜•best idea I've had so far
  useEffect(()=> dispatch({type: 'reset'}), [theProp]);
}
 

If you want to reset all the state, you can assign that prop the key of the component, and it will rebuild that component in it's original state

 

Right, yes thank you, I can push the problem up and reset with a key.

But you know, sometimes itโ€™s not the only state, and it would be nice to solve the problem internally in the component rather than lift the problem up, but yes, great idea, thanks

Hmmm, interesting. My brain's not going to let this one go... Could you provide a concrete example? and I'll let you know what I come up with.

Okay @wolverineks , here's a derived but concrete example:

Let's say you've got a component that lists the members of a team, and you can switch the teams and view the different members in the members-list.

In that list, each team member has a small UI state, to determine if the details view is showing, this doesn't seem appropriate to add to the team-member itself, because that object is provided from elsewhere (global/domain state), and it is really just a UI concern.

In the list of members, there is also a toggle with the option to "Hide Inactive Members", so you may not want to use the "reset with a key" method when the team changes, because you'd lose the "Hide Inactive Members" state.

So in the example above, you can look at the custom hook use-open-state.js and see that what I am doing is using useEffect to re-set the state for "what's open" whenever the list changes.

There are lot's of ways to solve this particular problem, keep a member mapping in state no matter what team shows, some crazy memoization, moving the "hide inactive members" state out of the members list (and use key to reset instead)... lot's of ways, it's not an insurmountable problem.

But my point was, I often find myself wishing useState() (or useReducer()) would take an inputs/deps argument like useEffect/useMemo/useCallback, so I could just reset my initial state based on a prop.

Something like...

useState( list.map(addExtraProp), [list] )

I think that would be swell.

I think the simplest solution to this problem is to Lift State Up no?

Well, personally I think useState() having a method of reseting state would be the 'simplest' solution...

useState(list.map(addExtraProp), [list]);

...but yes, I could lift the state out of the MembersList, and convert it to a fully controlled stateless component, and just reset the state in the handler for the Team's dropdown onChange. Definitely another good way to solve the problem.

I guess maybe I am the only one who ever felt like they wanted a way to reset the state in useState() or useReducer()

code of conduct - report abuse