Skip to content

Typescript: (5) Function Component State, Events, Event Handlers

Mithi Sevilla edited this page Mar 30, 2021 · 1 revision

useState

Typescript can infer types based on initial value otherwise:

const [count, setCount] = useState<number | null>(null)

useReducer

type  State  =  {
  count:  number
}

type Increment = {
  readonly type: "increment"
  readonly step: number
}

type Decrement = {
  readonly type: "decrement"
  readonly step: number
}

type Actions = Increment | Decrement

const reducer = (previous: State, action: Actions): State => {
  switch(action.type) {
    case 'increment': 
      return { count: previous.count + action.step }
    case 'decrement':
      return { count: previous.count - action.step }
  }
  return previous // this line should never be reached
  // but it makes typescript happy
}

IMPORTANT: Look at this error

type Double = {
  readonly type: 'double'
}

type Actions = Increment | Decrement | Double

const reducer = (previous: State, action: Actions): State => {
  switch(action.type) {
    case 'increment': 
      return { count: previous.count + action.step }
    case 'decrement':
      return { count: previous.count - action.step }
    default:
      neverReached(action)
      // ERROR: Argument of type 'Double' is not assignable to parameter of type 'never'
  }
  return Previous
}

const neverReached = (never: never) => {}

To remove the error replace your reducer with this

const reducer = (previous: State, action: Actions) =>  {
  switch(action.type)  {
    case 'increment':
      return { count: previous.count + action.step }
    case 'decrement':
      return { count: previous.count - action.step }
    case 'double':
      return { count: previous.count *  2}
    default:
      neverReached(action)
  }
  return previous // when you return this 
  // you don't actually need to specify the return type anymore
  // it can be inferred by typescript
}

const neverReached = (never: never) => {}

Now we can use it like this

type CounterProps = {initialCount: number}
const Counter = ({initialCount = 0}: CounterProps) => {
  const [state, dispatch] = React.useReducer(reducer, {count: initialCount})
  
  return <>
    {state.count}
     <button
       onClick={() => dispatch({type: "increment", step: 1})}>add one</button>
     <button
       onClick={() => dispatch({type: "decrement", step: 2})}>subtract one</button>
     <button
       onClick={() => dispatch({type: "double"})} >double</button>
  </>
}

You can also declare

const [state, dispatch] = useReducer<Reducer<State, Actions>>(initialState, reducer)

Events and Event Handlers

type Event = ChangeEvent<HTMLInputElement>
const Input = () => {
  const [value, setValue] = useState("")
  const handleChange = (e: Event) => setValue(e.currentTarget.value)

return <input 
    type="text"
    value={value}
    onChange={handleChange}
  />
}
type Event = ChangeEvent<HTMLInputElement>
type Props = {
  onSearch?: (criteria: string) => void
}

const Searchbox = ({ onSearch }: Props) => {
  const [criteria, setCriteria] = useState("")
  const handleChange = (e: Event) => {
    setCriteria(e.currentTarget.value)
    if(onSearch) {
      onSearch(e.currentTarget.value)
    }
  }
  
  return <input 
    type="text"
    value={criteria}
    onChange={handleChange}
  />
}

Other examples of events

type Event = React.KeyboardEvent<HTMLInputElement>
type AnotherEvent = React.FormEvent<HTMLFormElement>