Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Must a React reducer be a pure function?

I wrote a UI element as a function component which uses React's userReducer hook and it seems to run without errors.

useReducer references a function I wrote (called, imaginatively, reducer):

  const [state, dispatch] = React.useReducer(reducer, inputData,
    (inputData) => initialState(inputData));

There's state data which is input and output by the reducer function; and there are "managed" UI elements which depend on state, something like ...

  return (
    <div>
      <div>
        {state.elements.map(getElement)}
      </div>
      <ShowHints hints={state.hints} inputValue={state.inputValue} />
    </div>
  );

... which is normal.

My concern is that the reducer function isn't pure.

  • Its behaviour depends only on its input parameters -- so calling it twice with the same input parameters has the same resulting effect.
  • However it produces a side-effect, it doesn't only return new state

The side-effect is that there is an <input> element whose state is controlled by one of these:

const inputRef = React.createRef<HTMLInputElement>();

The <input> control is only semi-managed, something like this:

<input type="text" ref={inputRef} onKeyDown={handleKeyDown} onChange={handleChange}

The onKeyDown and onChange events are actions dispatched to the reducer (which is good) but the reducer is passed the HTMLInputElement instance (i.e. the inputRef.current value) as an input parameter, and the reducer sets properties of that HTMLInputElement to mutate its state -- that is instead of the <input> being a fully-managed component whose content is defined by the state that's output from the reducer.

The reason why the <input> element isn't fully-managed is that I need to control the selection range (i.e. start and end) within the <input> and not only its text value.

Questions:

  • Is it OK (e.g. bug-free albeit questionable or remarkable) for a reducer function to be impure in this way?
    • It does only depend on its input parameters and is therefore repeatable
    • But it mutates something (i.e. inputRef.current properties) as well as returning new state value
  • Is there another way for a reducer to control the start and end properties of an <input type="text"> element, e.g. a way to define the <input> element such that its start and end values are controlled by he state returned by a reducer?

(I think @Fyodor's answer below answers the second question, I'm still not sure about the first question).


What dictates the values that are to be set on the HTML element? Does the use information passed in or does it contain the logic?

The component's design and source code are shown here, and quite long.

It's a complicated "component" which is implemented using several elements -- a couple of <div>s, several <span>s, some clickable <svg>s, and the <input> element.

The reducer is given, as its input parameters:

  • The previous "state"
  • The current <input> instance (from which it can read the current state of the <input>)
  • The "action" created by an event handler

Two of the several event handlers or actions are the onKeyDown and onChange events of the <input> so the current state of the <input> is passed-in to the reducer when there's an event which changes the state of the <input>.

like image 858
ChrisW Avatar asked Nov 07 '22 15:11

ChrisW


1 Answers

Technically reducer may work with different side effects in it. I don't think it is a good practice, at least due to bad separation of concerns (it is expected that reducer only produce new state based on action, but not mutate something else). (Sorry for presenting my own opinion, as it seems, that side effects in useReducer are used, like here). Also in Redux, all side effect are moved to action creators.

For your specific question I may recommend to move inputRef mutation to separate useEffect hook, which in turn will depends on state like below

useEffect (() => {
    inputRef.currect // do work with inputRef
}, [state]); // make dependent from state

Here is sample of making useEffect depended form state

You can also move useEffect to custom hook to make code reusable like below (not tested, use as hint)

function mutateRef (inputRef: React.RefObject<HTMLInputElement>, state: /* type of state */) {
    useEffect (() => {
        inputRef.currect // do work with inputRef
    }, [state, inputRef]);
}
like image 154
Fyodor Avatar answered Nov 15 '22 06:11

Fyodor