Use class reference for state in useState hook

Viewed 503

I'd like to store a reference to a class in state but an error occurs.

Specifically, the following:


useState(SomeClass)

results in:

 Class constructor SomeClass cannot be invoked without 'new'

...

I can find another route for my specific need but I'd like to understand what the issue is here exactly.

--

FWIW here's a concrete situation where the above would be useful:

Consider a component that displays a list of Super[] but filtered by subclass. The component contains a set of radio buttons to set the filtering. e.g.

const FilterableListView = (props:{items:SuperType[]})=>{

const [SelectedType,setType] = useState(DefaultType as SuperType)// where DefaultType extends SuperType

const itemsToDisplay = props.items.filter(item=>item instanceof SelectedType) 

// in radio button callback somewhere...
setType(SomeOtherSubType)

...

}

Here the state is the filtering criteria. No need for instances of any of the class references.

Again, I can skin this cat in many ways — I just would like to know why useState(Class) results an attempt to instantiate Class.

2 Answers

The reason you cannot use a class as state is because useState allows for Lazy initial state.

The initialState argument is the state used during the initial render. In subsequent renders, it is disregarded. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render:

// lazy initial state example
const [state, setState] = useState(() => {
  const initialState = someExpensiveComputation(props);
  return initialState;
});

One possible solution is to wrap your class in an object, like { maker: MyClass }. Here is a minimal complete example. Click the create to create objects. Click ⚫️ or ⬛️ to change the type of object that is created.

class Circle {
  constructor() { this.value = "⚫️" }
}

class Square {
  constructor() { this.value = "⬛️" }
}

function App() {
  const [mode, setMode] = React.useState({ maker: Circle })
  const [elements, setElements] = React.useState([])
  return <div>
    <button
      onClick={_ => setMode({ maker: Square })}
      disabled={mode.maker === Square}
      children="⬛️"
    />
    <button
      onClick={_ => setMode({ maker: Circle })}
      disabled={mode.maker === Circle}
      children="⚫️"
    />
    <button
      onClick={_ => setElements([...elements, new mode.maker()])}
      children="create"
    />
    {elements.map(e => <div className="element">{e.value}</div>)}
  </div>
}

ReactDOM.render(<App/>, document.querySelector("#app"))
.element {
  display: inline-block;
  margin: 5px;
}
button[disabled] {
  border-color: red;
  border-radius: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="app"></div>

The useState() hook supports lazy initialization, which means that if it receives a function, it will call it on the first (and only) render. The returned value would be used as the initial state.

To circumvent that, pass a function that returns the class as the initial state:

const [SelectedType, setType] = useState(() => SomeClass)

As noted by よつば, the same thing would happen when you'll use setType to replace the class, since the setX function also supports functional updates. To update the state call:

setType(() => AnotherClass)
Related