DEV Community

React Context with useReducer and Typescript.

Elizabeth Alcalá on March 22, 2020

Just the code? There are many options to handle state in react applications. Obviously you can use setState for some small logic, but what if you ...
Collapse
 
infosec812 profile image
Deven Phillips

Finally! A Context/Reducer tutorial which uses typescript and DOESNT just show how to change the theme name... Any chance you would be willing to add a custom Dispatcher which could resolve promises from REST API Calls? All of our API calls return Promises, and the dispatch method in React cannot resolve Promises, so I would love to see an example of a "Dispatcher" which wraps the default dispatch function to handle resolving Promises!

Collapse
 
dipique profile image
Daniel Kaschel

It's not a best practice to include your API calls in your reducer. The purpose of your reducer is to accept incoming state changes and apply them to the context.

Instead, when an action occurs in a .tsx component, write code like this:

apiCallFunction(args).then(result => dispatch(Actions.ThisAction, result))

This will cause the context to be updated when the api call finishes, and then your reducer can apply the results, update the context, and React will handle updating the DOM.

Collapse
 
mannguyen0107 profile image
Man Nguyen

Greate post! although I have a question the way you have the type right now with the actions being a Discriminated unions of all the actions that created by ActionMap of all the reducers. This is very hard to manage when you have alot of reducers. There is no separation of concern. How would I go about moving each reducer into its own files. Then would I import each and every actions from all the reducer so I can use discriminated unions? I think there should be a better way, but I'm not knowledgable to do that yet. Do you have any suggestion?

Collapse
 
elisealcala profile image
Elizabeth Alcalá

Thanks! Yes I know if you have more than three reducers the types for the main reducer can increase, and be hard to manage.
I don't know how to improve the action type, this action has to be a union of product and shoppingCart actions because when using dispatch you can use either of both actions. To know exactly what action I need to use, maybe you could try with generics and conditional types. I think you could pass a generic type through Context to enable just certain types for actions and state. It's an idea, maybe works. I'll try to implement it.

Collapse
 
mannguyen0107 profile image
Man Nguyen

So I found this on a stack overflow post

function combineReducers(reducers) {
    return (state = {}, action) => {
        const newState = {};
        for (let key in reducers) {
            newState[key] = reducers[key](state[key], action);
        }
        return newState;
    };
}
Enter fullscreen mode Exit fullscreen mode

all you have to do is pass an object with key and value is the reducer. I'm searching about generic to convert this function to be type-safe. But if you have any idea please share. Thank you!

Collapse
 
mannguyen0107 profile image
Man Nguyen

As it turned out your way of doing the combine reducers also works for type-safe all you gotta do is use type assertion ie:

const mainReducer = ({ products, shoppingCart }: InitialStateType, action: ProductActions | ShoppingCartActions) => ({
  products: productReducer(products, action as ProductActions),
  shoppingCart: shoppingCartReducer(shoppingCart, action as ShoppingCartActions),
});
Enter fullscreen mode Exit fullscreen mode

By using the 'as' keyword there you can now get rid of the unions type on the action arg of your reducers

Thread Thread
 
elisealcala profile image
Elizabeth Alcalá

Hey, you are right, type assertion works well in this case, it makes the code look cleaner without the union types.

Thanks a lot.

Collapse
 
utshp08 profile image
reymart

Finally, I've found the exact thing that I was looking for. However, I still have a question. Can you show me how to initialize state from child nodes using this approach. What I wanted to achieve is something that is similar on this:
const [state, dispatch] = useReducer(
SampleReducer,
initialState,
() => {
return { field1: sampleValue1, field2: sampleValue2}
}

)

Thank you in advance.

Collapse
 
elisealcala profile image
Elizabeth Alcalá

Hey, I'm glad this helps you, according to react docs you can initialize a state passing a third argument. reactjs.org/docs/hooks-reference.h....

Collapse
 
utshp08 profile image
reymart

Yup, it same as what I also mentioned above. But, how would you achieve it in a context since the reducer is already part of the context?

Thread Thread
 
elisealcala profile image
Elizabeth Alcalá

oh. maybe you can pass the function through the context to the reducer? I'm going to try it and if I'm able to do it, I'll share the code :)

Thread Thread
 
utshp08 profile image
reymart

Yup, looking forward to it. That is what I wanted to achieve, I saw also in hooks document that we can create an init function that can be call through dispatch, however, not yet tried it.

Collapse
 
ekaczmarek profile image
Ela Kaczmarek

Amazing tutorial that helper in my current project!
I have one question: Is it possible to add new type in the same reducer ->Types.Get? It would be returning one product by incoming id. The input would be like in Types.Delete action.payload.id. It could be required to extend reducers operation in order to show the user one product.

Probably change of state would be required but I may be wrong:
const initialState = {
products: [],
selectedProduct: -1,
shoppingCart: 0,
}

Collapse
 
imadev profile image
imadev

Very good article, help me a lot. Just to add some salt, I do prefer to manage my reducers indexing an object instead of the traditional function with switch. Example below:

const handlers: Handlers = {
  LOGIN: (state, action) => {
    const { email, password } = action.payload!

    return {
      ...state,
      isAuthenticated: true,
      user: { email, password },
    }
  },
  LOGOUT: (state) => ({
    ...state,
    isAuthenticated: false,
    user: null,
  }),
  REGISTER: (state, action) => {
    const user = action.payload!

    return {
      ...state,
      isAuthenticated: true,
      user,
    }
  },
}


export const initialState: AuthContextType = {
  isAuthenticated: false,
  user: { name: '', email: '', password: '' },
}

type Handlers = {
  [key: string]: (
    state: AuthContextType,
    action: AuthActions,
  ) => AuthContextType
}

const authReducer = (state: AuthContextType, action: AuthActions) =>
  handlers[action.type] ? handlers[action.type](state, action) : state

export { authReducer }
Enter fullscreen mode Exit fullscreen mode

The missing types are already exposed in the article.

Collapse
 
karniej profile image
Paweł Karniej

Great Article, thank you for that - I Was struggling with correct typings for store a little bit too long :P

Collapse
 
mannguyen0107 profile image
Man Nguyen

Hi do you know how do I apply middleware with this config? I have been looking into this and i only got as far as create a single middleware and apply it but can't find how to apply multiple middleware

Collapse
 
elisealcala profile image
Elizabeth Alcalá

I haven't tried to apply multiple middlewares, let me try, and if I found a way I'll let you know.

Collapse
 
raphaelmansuy profile image
Raphael MANSUY

Great post !

Collapse
 
vcardins profile image
Victor Cardins

Excellent post, very well written!! It fits perfectly to what I'm doing here!! :claps

Collapse
 
thepedroferrari profile image
Pedro Ferrari

Your article made it clearer to me a few things I was unsure about. Thank you @elisealcala , well done!

Collapse
 
gindesim profile image
gindesim

how the create and delete works?
your code is not complete.

Collapse
 
elisealcala profile image
Elizabeth Alcalá

Hi, I update the code here. codesandbox.io/s/context-reducer-t..., check the List component. Basically, I create a state to handle the form and then just list the products.

import { AppContext } from "./context";
import { Types } from "./reducers";

const { state , dispatch } = React.useContext(AppContext);

const createProduct = () => {
  dispatch({
    type: Types.Create,
    payload: {
      id: Math.round(Math.random() * 10000),
      name: form.name,
      price: form.price
    }
  });
};

const deleteProduct = (id: number) => {
  dispatch({
    type: Types.Delete,
    payload: {
      id,
    }
  })
}
Collapse
 
damianesteban profile image
Damian Esteban

This is excellent. I love that it is fully typed.

Collapse
 
amansethi00 profile image
Aman Sethi

Thank You for this,context+reducer works beautifully in my code now

Collapse
 
andemosa profile image
Anderson Osayerie

Great post. Thanks for this

Collapse
 
dinashchobova profile image
Dina Shchobova

Great article! It would be nicz to have an article on how to test it

Collapse
 
kshindod17 profile image
kshin-DOD17

Thank you this was very helpful - I appreciate the example!!

(you have a typo on the last slide
useContex --> useContext

Collapse
 
dinashchobova profile image
Dina Shchobova • Edited

Great article! It would be nice to have an article on how to test it

Collapse
 
imakimaki profile image
AntiLun • Edited

Could anyone tell me why type ActionMap need to extends { [index: string]: any }? Thx so much.