Tarang Hirani
Tarang Hirani

Reputation: 590

React TS useContext useReducer hook

I can't figure out what the type error is in this code

import React from 'react';

interface IMenu {
  items: {
    title: string,
    active: boolean,
    label: string
  }[]
}


type Action =
  | { type: 'SET_ACTIVE', label: string }

const initialState: IMenu = {
  items: [
    { title: 'Home', active: false, label: 'home' },
    { title: 'Customer', active: false, label: 'customer' },
    { title: 'Employee', active: false, label: 'employee' },
    { title: 'Report', active: false, label: 'report' },
  ]
}

const reducer = (state: IMenu = initialState, action: Action) => {
  switch (action.type) {
    case 'SET_ACTIVE': {
      const label = action.label;
      const items = state.items.map((item) => {
        if (item.label === label) {
          return { ...item, active: true };
        }
        return { ...item, active: false }
      })
      return { items }
    }
    default:
      throw new Error();
  }
};

export const MenuContext = React.createContext(initialState);
export const MenuConsumer = MenuContext.Consumer;

export function MenuProvider(props: any) {
  const [state, dispatch] = React.useReducer(reducer, initialState)
  const value = { state, dispatch };
  console.log(value)
  return (
    <MenuContext.Provider value={value}>
      {props.children}
    </MenuContext.Provider>
  )
}

The error I receive is this

    /Volumes/Tarang External/react-context-typescript/src/context/index.tsx
TypeScript error in /Volumes/Tarang External/react-context-typescript/src/context/index.tsx(49,27):
Property 'items' is missing in type '{ state: { items: { active: boolean; title: string; label: string; }[]; }; dispatch: Dispatch<{ type: "SET_ACTIVE"; label: string; }>; }' but required in type 'IMenu'.  TS2741

    47 |   console.log(value)
    48 |   return (
  > 49 |     <MenuContext.Provider value={value}>
       |                           ^
    50 |       {props.children}
    51 |     </MenuContext.Prov

Can someone help point out what exactly I'm doing wrong? I am new to typescript so please guide me if necessary. I am trying to pass the context value of state and dispatch to the child component. I am not sure what's going. Is this also a correct way to implement useContext and useReducer hooks?

Upvotes: 6

Views: 8081

Answers (2)

ford04
ford04

Reputation: 74490

I am trying to pass the context value of state and dispatch to the child component.

The MenuContext type is determined by initialState - they don't match up in your case. You could declare a custom context value type:

type StoreApi = {
  state: typeof initialState
  dispatch: React.Dispatch<Action>
}

Then define MenuContext like this:

// undefined is just the default in case, you don't have a provider defined
export const MenuContext = React.createContext<StoreApi | undefined>(undefined)
// or if you want it more like your first example (I would prefer first variant)
export const MenuContext = React.createContext<StoreApi | typeof initialState>(initialState)

Is this also a correct way to implement useContext and useReducer hooks?

Above is a common pattern to initialize a React context. Further extension points:

1.) Currently, all your context consumers re-render each render phase, as value is a new object reference every time. If you have slow performance (no premature optimization), memoize value:

const value = React.useMemo(() => ({ state, dispatch }), [state])

2.) Create a custom useMenu hook, that returns state, dispatch and other useful API methods.

function useMenu() {
  const context = React.useContext(MenuContext);
  if (context === undefined) throw new Error(`No provider for MenuContext given`);
  const { state, dispatch } = context;
  const myApiMethod = () => dispatch({ type: "SET_ACTIVE", label: "foo" })
  return { ...context, myApiMethod };
}

const Client = () => {
  const { state, dispatch, myApiMethod } = useMenu()
  // ...
}

This way MenuContext is encapsulated, there is no need to use React.useContext(MenuContext) and the client gets a tailored API. Further reading: How to use React Context effectively and its related articles.

Upvotes: 4

Tejas
Tejas

Reputation: 711

Your problem is that your context is supposed to have the same type as initialState, but instead, you set your context's default value to { state, dispatch }.

This is incorrect.

To solve, either set your context's default type to { state, dispatch } (I think this is what you want, or set the context's default type to typeof initialState.

Here's the solution.

Upvotes: 6

Related Questions