Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

React & TypeScript: Avoid context default value

In the effort to better learn React, TypeScript, and Context / Hooks, I'm making a simple Todo app. However, the code needed to make the context feels cumbersome.

For example, if I want to change what a Todo has, I have to change it in three places (ITodo interface, default context value, default state value). If I want to pass down something new, I have to do that in three places (TodoContext, TodoContext's default value, and value=). Is there a better way to not have to write so much code?

import React from 'react'

export interface ITodo {
    title: string,
    body?: string,
    id: number,
    completed: boolean
}

interface TodoContext {
    todos: ITodo[],
    setTodos: React.Dispatch<React.SetStateAction<ITodo[]>>
}

export const TodoContext = React.createContext<TodoContext>({
    todos: [{title: 'loading', body: 'loading', id: 0, completed: false}],
    setTodos: () => {}
})

export const TodoContextProvider: React.FC<{}> = (props) => {
    const [todos, setTodos] = React.useState<ITodo[]>([{title: 'loading', body: 'loading', id: 0, completed: false}])

    return (
        <TodoContext.Provider value={{todos, setTodos}}>
            {props.children}
        </TodoContext.Provider>
    )
}
like image 468
ZeroSevenTen Avatar asked Apr 20 '20 22:04

ZeroSevenTen


People also ask

What is React used for?

It is a library that allows us to create native mobile apps in Android and iOS using React. js. In 2016, with version 15, React. js started using Semantic Versioning.

Is React for HTML or js?

To get an overview of what React is, you can write React code directly in HTML. But in order to use React in production, you need npm and Node. js installed.

What is in React means?

1 : to exert a reciprocal or counteracting force or influence —often used with on or upon. 2 : to change in response to a stimulus. 3 : to act in opposition to a force or influence —usually used with against. 4 : to move or tend in a reverse direction. 5 : to undergo chemical reaction.

Is React Java or JavaScript?

React is a JavaScript-based UI development library. Facebook and an open-source developer community run it. Although React is a library rather than a language, it is widely used in web development. The library first appeared in May 2013 and is now one of the most commonly used frontend libraries for web development.


4 Answers

There's no way of avoiding declaring the interface and the runtime values, because TS's types disappear at runtime, so you're only left with the runtime values. You can't generate one from the other.

However if you know that you are only ever going to access the context within the TodoContextProvider component you can avoid initialising TodoContext by cheating a little bit and just telling TS that what you're passing it is fine.

const TodoContext = React.createContext<TodoContext>({} as TodoContext)

If you do always make sure to only access the context inside of TodoContextProvider where todos and setTodos are created with useState then you can safely skip initialising TodoContext inside of createContext because that initial value will never actually be accessed.

like image 110
Aron Avatar answered Oct 03 '22 22:10

Aron


Note from the react documentation:

The defaultValue argument is only used when a component does not have a matching Provider above it in the tree.

The way I prefer to do it is by actually specifying that the default value can be undefined

const TodoContext = React.createContext<ITodoContext | undefined>(undefined)

And then, in order to use the context, I create a hook that does the check for me:

function useTodoContext() {
  const context = useContext(TodoContext)
  if (context === undefined) {
    throw new Error("useTodoContext must be within TodoProvider")
  }

  return context
}

Why I like this approach? It is immediately giving me feedback on why my context value is undefined.

For further reference, have a look at this blog post by Kent C. Dodds

like image 38
marinvirdol Avatar answered Oct 03 '22 21:10

marinvirdol


After awhile, I think I've found the best way to go about this.

import React from 'react'

export interface ITodo {
    title: string,
    body?: string,
    id: number,
    completed: boolean
}

const useValue = () => {
    const [todos, setTodos] = React.useState<ITodo[]>([])

    return {
        todos,
        setTodos
    }
}

export const TodoContext = React.createContext({} as ReturnType<typeof useValue>)

export const TodoContextProvider: React.FC<{}> = (props) => {
    return (
        <TodoContext.Provider value={useValue()}>
            {props.children}
        </TodoContext.Provider>
    )
}

This way, there is single point of change when adding something new to your context, rather than triple point of change originally. Enjoy!

like image 31
ZeroSevenTen Avatar answered Oct 03 '22 21:10

ZeroSevenTen


My situation might be a little different than yours (and I realize there's already an accepted answer), but this seems to work for me for now. Modified from Aron's answer above because using that technique didn't actually work in my case.

The name of my actual context is different of course.

export const TodoContext = createContext<any>({} as any)

like image 36
Greg Clark Avatar answered Oct 03 '22 21:10

Greg Clark