What is the scope of React Context? - reactjs

I am not clear on where Context can be created in a React app. Can I create context anywhere in the component tree, and if so, is that context only scoped to children of where it is created? Or is Context inherently global?
Where in the documentation can I find this?
Case: I'm reusing a component multiple times on a page and would like to use context to handle data for sub-components, but that context needs to be unique to each sub-tree.

There are 2 separate things: The context object, and the context provider.
The context object is created once globally (with an optional default value, which is global if no provider was passed from a component parent):
const FontContext = createContext('Courier');
While a context provider actually passes down its own local override of the context, which only applies to its children:
<FontContext.Provider value='Consolas'>
{children}
</FontContext.Provider>
The interesting part is that contexts cascade:
<>
<MyFontConsumer /> // value is 'Courier', the default
<FontContext.Provider value='Consolas'>
<MyFontConsumer /> // value is 'Consolas'
<FontContext.Provider value='TimesRoman'>
<MyFontConsumer /> // value is 'TimesRoman'
</FontContext.Provider>
<MyFontConsumer /> // value is 'Consolas'
</FontContext.Provider>
</>
To consume the context in your component you can use the useContext() hook, (you can use the special <FontContext.Consumer> component or MyClassComponent.contextType instead) which requires you to pass the same object.
My preferred way to avoid having to pass around the context object is to keep them all in the same component and export a custom hook:
const FontContext = createContext('Courier')
export function FontProvider({value, children}) {
return <FontContext.Provider value={value}>
{children}
</FontContext.Provider>
}
export function useFont() {
return useContext(FontContext)
}
Now you can use it in your component:
import {FontProvider, useFont} from './FontProvider'
function App() {
const font = useFont() // value is 'Courier', the default
return <FontProvider value='TimesRoman'>
<MyChild />
</FontProvider>
}
function MyChild() {
const font = useFont() // value is 'TimesRoman'
...
}

I am not clear on where Context can be created in a React app. Can I create context anywhere in the component tree?
A context object can technically be created anywhere with createContext, but to actually set the value to something other than the default you need a provider component. The context will be available via its consumer or the useContext hook in any child components of the provider.
Is that context only scoped to children of where it is created?
If by "created" you mean where the provider is located, yes. However with the new context API you can set a default value, even without a provider.
Or is Context inherently global?
No not inherently, though contexts are often placed within or above the app's root component to become global for all practical purposes.
Where in the documentation can I find this?
Context
useContext hook
Case: I'm reusing a component multiple times on a page and would like to use context to handle data for sub-components, but that context needs to be unique to each sub-tree.
As far as I understand, you have two options here:
Use a single context with different providers overriding it in sub-trees
Have a different context for each sub-tree

This is a good question. I'd like to share something about the scope of the context especially.
Context
Context actually is a "global" idea, because at the time you create it, it's not attached to any fiber/component.
const UserContext = React.createContext()
export default UserContext
That's why it's normally exported right away. Why? Since all providers and consumers need to access it in different files under normal conditions. Although you can put them in a same file, that really doesn't show off the context nature. It's made to hold a "global" value that is persisted for the entire application. The underlying value is only storing a temporary one as the scope input argument described below. The real value is hidden behind the scene, because it keeps changing depending on the scope.
Scope
Context might be created long before the hook. There's no other way for React to send a data into certain distance deeper without showing up in the props. And a context is designed similar to a function, internally React uses a stack to pop/push context as it enters or leaves a provider. Therefore you'll see similar behavior when the same context provider is nested like below.
const Branch = ({ theme1, theme2 }) => {
return (
<ThemeContext.Provider value={theme1}>
// A. value = theme1
<ThemeContext.Provider value={theme2}>
// B. value = theme2
</ThemeContext.Provider>
// C. value = theme1
</ThemeContext.Provider>
)
}
If you treat each ThemeContext as a function with input argument value, then it's clear what are the values for locations A, B and C. In a way, a context is no different as a function :) This is also a good example to show you the prop value isn't the same value that the context ThemeContext is tracking, because otherwise you'll end up with a fixed value, either theme1 or theme2. Behind the scene, there's a global mechanism :)
NOTES:
Context is designed to be very powerful, however due to the over-rendering issue, it's never reached to that level. But i guess there's no other replacement in React to share variables, therefore there's tendency that it'll become popular again, especially with the introduction of useContextSelector under proposal. I put some explanation to above in a blog here, https://windmaomao.medium.com/react-context-is-a-global-variable-b4b049812028

Related

Should React Context be used instead of props drilling for non global data?

I have found myself in a situation where the child components odf my app take some props with the only purpose of passing them down to their own child components, props drilling. It is clear that Context is a good alternative for data that is mean to be globally accessed in the application. However, is it a good practice to create sub-contexts for subsections of the application? In this case, the data stored in a section´s context is only going to be used for that particular section. My worry is that context may be intended to be used globally only.
//This context provider wrapps the whole application. The data stored there will be //accessible down to the last child of App.js.
<AppContextProvider>
<App/>
<AppContextProvider/>
Now inside App.js
const App ()=>{
return (
<div>
<subcompoenet1/>
<subcomponent2/>
<div/>
)
Now subcmpoenet2 is a complex section of the app. The data flowing in this section will only be accessible to its children. So is it a good practice to create a context for this section? Just like so:
const subcomponent2 ()=>{
return (
<AsectionContextProvider>
<child1/>
<chile2/>
<chile3/>
<AsectionContextProvider/>
)
}
<AppContextProvider>

How to initialize and use context in the same component?

I have a context called SortContext. What I would like to do is initialize this context (create its provider) and then use that context in the same component. Is this possible?
For example:
export default function MyComponent ({children}) {
const mySortValue = useContext(SortContext)
return (
<SortContext.Provider value={'exampleValue'}>
{children}
</SortContext.Provider>
)
}
In this component, the variable mySortContext will not have access to the value 'exampleValue', as this context is not created until after the useContext hook.
Alternatively:
export default function MyComponent ({children}) {
return (
<SortContext.Provider value={'exampleValue'}>
<SortContext.Consumer>
{context => {
const mySortValue = useContext(SortContext)
return children
}}
</SortContext.Consumer>
</SortContext.Provider>
)
}
Something like this doesnt work, as the function cannot use react hooks.
I could obviously just create a new component, put it within the <SortContext.Provider> tags, and access the context there, but is there any way to do it all in one component?
//Sidenote
To give some background on why I want this, I use the context to establish some information about how data should be sorted. I would like any components within the context to have access to this data. In the case where I want a simple button, list of data, and sort function in a single component, it seems like overkill to create two components; one to feed it the sort context, the other to host the sort buttons and data.
This is not possible without creating another component.
This is mentioned in the "Pitfall" box in the react js beta docs here (scroll down a bit) :
https://beta.reactjs.org/apis/usecontext#passing-data-deeply-into-the-tree
What I would like to do is initialize this context (create its provider) and then use that context in the same component. Is this possible?
In a sense, yes, but you'd do it through local variables, not context.
You're already writing code to generate the value that you'll pass into the provider. In your example, that's just value={'exampleValue'}, but even in a more complicated realworld case, it's entirely under your control what value you pass in. Simply save that value in a local variable, and use it anywhere else you need it.
export default function MyComponent ({children}) {
// Replace the following with whatever complicated logic you need to generate the value
const mySortValue = 'exampleValue';
// Do stuff with mySortValue here
return (
<SortContext.Provider value={mySortValue}>
{children}
</SortContext.Provider>
)
}

React / Context API / TypeScript : How to init web app and avoid UI flickering at statup?

When a user navigate to my site, I need to initialize the React web app as follow:
If the incoming user has never requested the web app, I want to set the UI language to browser default (navigator.language).
If the incoming user has already visited the site and chosen a prefered language (lang stored in the localStorage), I want to init the UI with this language.
If the incoming user has an account and is already connected (token available in localStorage), I want to auto-connect him and render the app accordingly : login button transformed into a welcome message, UI language set to user preference.
To do so, I'm using React Context API and a defaultUser object.
defaultUser: init a default user
const defaultUser = {
language: 'en_EN',
isConnected: false
}
Context: create a default context
export const AppContext = createContext({
connectedUser: defaultUser,
})
Provider: create the provider with default context
export function AppProvider({ children }: any) {
[...]
const provider = {
connectedUser
}
return (
<AppContext.Provider value={provider}>
{children}
</AppContext.Provider>
)
}
App: init the provider during app start up
export class App extends Component {
static contextType = AppContext
render() {
return (
<AppProvider>
<AppContainer />
</AppProvider>
)
}
}
AppContainer: render the app
export class AppContainer extends Component {
static contextType = AppContext
componentDidMount() {
/** If user is not connected, verify if a stored session exists and use it to connect user */
if (!this.context.connectedUser.isConnected) {
[...do things...]
}
}
The whole mecanism works well except an annoying thing : the web app is systematically initialized with default user values, until the AppContainer::componentDidMount() do the real init job.
This is causing a sort of flickering effect.
I'm struggeling for 2 days on how to fix that, trying to perform Context init before <AppContainer /> rendering, and I'm stuck.
Any recommandations?
EDIT :
For clarity, I'm adding a diagram. Currently :
React App is rendered at start.
Context is initialized at start with default value.
Context is updated when end is reached.
React App is rendered again when end.
Any layout change during these two steps (UI language, UI modification based on user permissions) are clearly visible to the user and generate a sort of flickering.
I found sort of a solution by simply conditionning <AppContainer/> loading, postponing it to the end of the sequence. However instead of having flickering I have now a lag and other unwanted side effects.
The goal would be to differ all the sequence before React Act is rendered, and after Window is available. Then dynamically create the Context, then render all.
I think the point would be resolved if I could dynamically create the AppContext and pass a variable to createContext() during App constructor() or maybe componentWillMount() (not sure when Window is available), but then TypeScript get into play with types issues and I'm still stuck.
You didn't share the code that initializes the context, but I suspect you put the default value to be either a hardcoded value, or navigator.language and therefore experience the flickering. I'd like to suggest two ways to solve this:
Solution 1
Perhaps instead of having a hardcoded default context you could generate the default context programmatically by accessing localStorage.get('lang') or similar? There is a slight drawback to this solution though: You will be mixing concerns of react and the browser, but I think in this case it's an alternative to consider, because it's very simple and obvious to the reader.
Solution 2
Alternatively, when calling ReactDOM.render you could pass down whatever you need from localStorage as a prop to your application and so you keep the browser related logic separate from the pure React stuff.
I hope this makes sense.
Here's my follow-up after Amit suggestions, in case it can help anyone else.
Init Context with functions
Instead of initializing defaultUser with hard-coded values and update it later, I set directly it with a function returning navigator.lang as suggested. This solved the flickering issue on UI labels.
Init data before RectDOM.render
However I still had flickering on UI components for which I have to get the appropriate state from an API call.
Eg, if the incoming user has a valid session token stored in localStorage, the Login button must be disabled. Before doing so, I need to make sure the session token is valid by an async call to the API. I didn't find a way to have it «awaited» by the Context init which seems to be synchronous.
That's where Amit second suggestion get into play. Instead of struggling finding a solution inside React, I did necessary processing before ReactDOM.render, then passing stuffs as props to <Apps/>.
This works pretty well to get and pass the data...
Except that Context API didn't setSate anymore as soon as any of its data was refering to an object from outside the Context. In other word using function calls is ok to init (probably by val), but reference to external objects breaks setState.
Conclusion
As my project is still in early stage, this gave me the chance to get rid of Context API, do the proper init as required, and code the props/states progagation with basic React.

React hook - Can I use common variable out of function(hook) or Is it anti-pattern?

I recently use react hook well. however sometime I wonder my coding style is okay.
let mDown = false;
let timerId = null;
function App() {
useEffect(() => {
mDown = true
....
}, [])
}
Like bottom code, I declare variable out of function. and I want to use common variable and access, change this value without using state.
Is it anti-pattern?
Thanks to read. :)
yes, of course. you can use the common variable in not only the hook function, but also any other methods of the App.
however the best practice is to make the global variables in other js files like constant.js, and import them in the above of the file.
import { mDown, timerId } from '../constant'
So let's think in terms of components. I mean, let's ignore for a while there barely will be more than one <App /> but think about component-centric approach in general.
Do you mean some data independent for each component's instance? Then you use useState(if changing this data should re-render component) or useRef(if updating this should never trigger re-rendering).
Or should that data be shared among all the components? Then, again it depends. Sometimes global variable is fine(real global variable, as a property on window object), sometimes better to put it into Context API or any state management layer(Mobx, Redux) instead.
Why don't have module-level variable as you aer going to? I see multiple reasons(again, maybe not big deal for exact case with <App />):
Module-level variable sticks to module instance. If we you ever end with multiple version of the same component in the same application, they will have own instances of such a variable. Think of <Dropdown /> component and some variable made to collapse all other instances once some is expanded. In case with module-level variable you may end some dropdown closes some instances - but not all - just if you got few versions of the same package used(like coming as 2nd level dependency)
No access, and no isolation - hard to unit-test.
And finally: what could be a profit here?

Shouldn't useContext() only be used for low frequent updates? (mobx-react-lite)

Almost all examples (even the official documentation) use mobx-react-light in combination with useContext() hook.
However, React, many articles and blog-posts advice to NOT USE useContext() for middle/high frequent updates. Isn't state something which can update very frequenty?
Should one use the package in combination with the hook or will performance issues occur?
useContext() is only used to get your store value (reference) and this value is not updated frequently, usually you only set your stores once and don't touch it after. When you use actions you only change observable values of your store and not the store itself. So basically Context is only used to pass reference to the store down the tree, after that all the work performed by MobX only.
Example from MobX docs:
import {observer} from 'mobx-react-lite'
import {createContext, useContext} from "react"
const TimerContext = createContext<Timer>()
const TimerView = observer(() => {
// Grab the timer from the context.
const timer = useContext(TimerContext) // See the Timer definition above.
return (
<span>Seconds passed: {timer.secondsPassed}</span>
)
})
ReactDOM.render(
<TimerContext.Provider value={new Timer()}
<TimerView />
</TimerContext.Provider>,
document.body
)
So you pass value of new Timer() to TimerContext.Provider once when you render your app and it will never be changed or updated after that. Even docs says:
Note that we don't recommend ever replacing the value of a Provider with a different one. Using MobX, there should be no need for that, since the observable that is shared can be updated itself.
However, if you don't have SSR or don't test your app, then you don't even need to use Context at all, you can just use global variables/singletons as your stores and it will work perfectly fine.
Example:
// Initialize timer somewhere
export const myTimer = new Timer()
// You can use directly in the same file or import somewhere else
import { myTimer } from './file-with-your-timer'
// No props, `myTimer` is directly consumed from the closure or from another file
const TimerView = observer(() => <span>Seconds passed: {myTimer.secondsPassed}</span>)
ReactDOM.render(<TimerView />, document.body)
Quote from docs:
Using observables directly works very well, but since this typically introduces module state, this pattern might complicate unit testing. Instead, we recommend using React Context instead.
More about best practices with React: https://mobx.js.org/react-integration.html

Resources