Do you want to request a feature or report a bug? Feature request / question
According to React docs, there are 2 ways to avoid passing props through many levels:
When using the new context API, a consumer component must know, and explicitly import, a context.
This raises a quite big disadvantage comparing to the legacy context API:
Such component can't be reusable with different contexts (unless making a prop only version of this component, and wrapping it with another one that uses the context directly).
In fact, it means that a component can't be "contextual" and reusable (by different contexts) at the same time.
Using composition in many cases feels wrong for solving this, quoting the docs:
_However, this isn’t the right choice in every case: moving more complexity higher in the tree makes those higher-level components more complicated and forces the lower-level components to be more flexible than you may want._
Example of a component I struggle to understand why it should now import a context:
import * as React from "react"
import * as propsTypes from "prop-types"
export class Link extends React.PureComponent {
static contextTypes = {
navTo: propsTypes.any
}
handleClick = (e) => {
e.preventDefault()
const {path} = this.props
this.context.navTo(path)
}
render() {
const {path, ...props} = this.props
return <a href={path} onClick={this.handleClick} {...props} />
}
}
If it was already discussed or answered, I apologize, couldn't find any related issues.
Such component can't be reusable with different contexts (unless making a prop only version of this component, and wrapping it with another one that uses the context directly).
^^ this is exactly what I would suggest (the bold part). I don't see anything wrong with it.
In fact, it means that a component can't be "contextual" and reusable (by different contexts) at the same time.
I don't see where the disadvantage is, and why being "reusable by different contexts" is valuable.
Maybe you could provide more information about how you used the old approach in practice, and why it's significantly more convenient? This would need to show more code (usage site as well) so we can compare them.
Before elaborating I want to add one more note:
My understating is that one of the main problems the new context API solves is the re-render problem that the legacy API had. But this problem is irrelevant for non-state context (callback functions, actions).
^^ this is exactly what I would suggest (the bold part). I don't see anything wrong with it.
Not wrong, but less flexible IMO, elaborating below.
I don't see where the disadvantage is, and why being "reusable by different contexts" is valuable.
Because any component that would use a contextual component, practically becomes non-reusable by different contexts too.
export class ContextualLink extends React.PureComponent {
render() {
return <Consumer>
({navTo}) => <Link navTo={navTo} path={this.props.path} />
</Consumer>
}
}
export class LoginForm extends React.PureComponent {
render() {
return <div>
<ContextualLink path="/sign-up">Sign Up</ContextualLink>
</div>
}
}
export class SignUp extends React.PureComponent {
render() {
return <div>
<ContextualLink path="/login">Login</ContextualLink>
</div>
}
}
Now LoginBox and SignUp are not reusable too because they use a contextual component. And if we would make a "configurable" (maybe passing link component as a prop) version of them too, it would feel like "passing props to all levels" again.
But with the legacy context API, all components are reusable:
export class Link extends React.PureComponent {
static contextTypes = {
navTo: propsTypes.any
}
handleClick = (e) => {
e.preventDefault()
const {path} = this.props
this.context.navTo(path)
}
render() {
const {path, ...props} = this.props
return <a href={path} onClick={this.handleClick} {...props} />
}
}
export class LoginForm extends React.PureComponent {
render() {
return <div>
<Link path="/sign-up">Sign Up</Link>
</div>
}
}
export class SignUp extends React.PureComponent {
render() {
return <div>
<Link path="/login">Login</Link>
</div>
}
}
Can you give me a complete example of where “reusing” with a different context is helpful?
Reusing within 2 different apps and 1 mock app (for testing), utilizing different routing implementations:
export class App1 extends React.PureComponent {
navTo = (path) => this.setState({path}, () => {
// https://github.com/ReactTraining/history
history.push(path)
})
getChildContext() {
return {navTo: this.navTo}
}
render() {
const {path} = this.state
return <Fragment>
{path === "/login" && <LoginForm/>}
{path === "/sign-up" && <SignUp/>}
</Fragment>
}
}
export class App2 extends React.PureComponent {
navTo = (path) => this.setState({path}, () => {
window.location.hash = path
})
getChildContext() {
return {navTo: this.navTo}
}
render() {
const {path} = this.state
return <Fragment>
{path === "/login" && <LoginForm/>}
{path === "/sign-up" && <SignUp/>}
</Fragment>
}
}
// test app, no window / browser
export class TestApp extends React.PureComponent {
navTo = (path) => this.setState({path}, () => {
console.log("path changed!")
})
getChildContext() {
return {navTo: this.navTo}
}
render() {
return <LoginForm/>
}
}
Why is rendering <NavigationProvider> with different values in App1, App2, and TestApp undesirable?
I knew I was missing something! Now I see what I done wrong. I kept using React.createContext in high level modules instead of in low level modules. Thank you!
No problem!
@kobiburnley could you please elaborate on your understanding ?
I found your thoughts pretty interesting in this thread but i'm not sure to follow your last comment.
How did you solve the problem of reusability you were previously describing ?
Thank you !
@GBarthos Sure
I will start by mentioning that I still find the implicitly of the legacy API a bit more elegant, but that not crucial for me.
After understanding where I was wrong about the new API I realized I can get the same result with it.
I had a wrong conception about where and when to use React.createContext.
In short, I always used it nearby the "Providers" (where the actual context data existed), instead of nearby the "Consumers"
In my example above I have 1 "consumer" component, Link, and 1 "provider" component, App.
Now, let's say Link is a separated module, with no dependencies, and App is a top level module that depends on the Link module. At first I done this:
app module:
import * as React from "react"
import {Link} from "./link"
export const {Consumer, Provider} = React.createContext({
navTo: () => {}
})
export class App extends React.PureComponent {
service = {
navTo: (path) => this.setState({path}, () => {
window.location.hash = path
})
}
render() {
return <Provider value={this.service}>
<Link path="somewhere" />
</Provider>
}
}
link module:
import {Consumer} from "./app"
export class LinkIO {
handleClick = (e) => {
e.preventDefault()
const {path, navTo} = this.props
navTo(path)
}
render() {
const {path, navTo, ...props} = this.props
return <a href={path} onClick={this.handleClick} {...props} />
}
}
export class Link extends React.PureComponent {
render() {
const {path, ...props} = this.props
return <Consumer>
{({navTo}) => <LinkIO path={path} navTo={navTo} {...props}/>}
</Consumer>
}
}
It is clear how I break modularity here (app depends on link and link depends on app), I think what confused me is the name "createContext", it tricked me to always put it where the actual instance of the context is. It actually does not create a context as its name implies, but utility components to provide and consume contexts.
This is how it should be, just the opposite, link doesn't depends on app:
import * as React from "react"
export const {Consumer, Provider} = React.createContext({
navTo: () => {
}
})
export class LinkIO {
handleClick = (e) => {
e.preventDefault()
const {path, navTo} = this.props
navTo(path)
}
render() {
const {path, navTo, ...props} = this.props
return <a href={path} onClick={this.handleClick} {...props} />
}
}
export class Link extends React.PureComponent {
render() {
const {path, ...props} = this.props
return <Consumer>
{({navTo}) => <LinkIO path={path} navTo={navTo} {...props}/>}
</Consumer>
}
}
import * as React from "react"
import {Link, Provider} from "./link"
export class App extends React.PureComponent {
service = {
navTo: (path) => this.setState({path}, () => {
window.location.hash = path
})
}
render() {
return <Provider value={this.service}>
<Link path="somewhere" />
</Provider>
}
}
To complement this, the explicit nature is intentional. The problem with the implicitness of the old API is that you start running into name clashes between different libraries and different parts of a large app because they share the same namespace. Kind of like with global variables.
ok it's now clearer, thank you both !
Most helpful comment
To complement this, the explicit nature is intentional. The problem with the implicitness of the old API is that you start running into name clashes between different libraries and different parts of a large app because they share the same namespace. Kind of like with global variables.