useContext / Create Portal: the disagreements
I really enjoy leading: Creating a group that works together harmonically, willing to ask, and happy to assist. Having the option to learn from others, the privilege to guide. I love to see the members grow, from the fear of the unknown to the endless opinionated discussion. But most of all, the satisfaction of creating something bigger than my individual abilities.
One of the responsibilities as the lead is to create an open culture of discussion and argument. And such a topic came this week, on a minor problem: how to allow an inner element to control an element outside its scope. This post is not about the best solution, since, and here is a spoiler alert, the Portal is the official solution for that problem. This post is about the discussion and the process.
The difference between managing and leading is the willingness to have a good argument, and lose.
Me 🙂
The Problem
Let’s say we have a page with a global layout, but some inner component needs to affect something outside its scope. In this example we have some cat app:
- Page layout
- Buttons list
- Page title
- Cat rout
- Some cat
The problem we had is that each cat had its own buttons list, so the top buttons
component is affected by the specific cat been selected.
const App = () => { return <PageLayout /> } const PageLayout =( ) => { return ( <div> <Buttons buttons={[]} /> <h1>Title</h1> <SetCat /> </div> ) } const SetCat = ({}) => { switch ("fatCat"){ case "smallCat": return <SmallCat /> case "fatCat": return <FatCat /> default: return <Cats /> } } const Buttons = ({ buttons }) => { return <div className="modal"> {buttons.map(b => <button key={b}>{b}</button>)} </div>; } // The different cat components const FatCat = ({ }) => { const buttons = ['Weight issues', 'Close', 'Export']; return <img src="http..." /> } const SmallCat = ({ }) => { const buttons = ['Size issues', 'Close', 'Export']; return <img src="http..." /> } const CatsList = () => { const buttons = ['Select a cat']; return <img src="http..." /> }
Solution I: Passing the Setter
After one of my members pointed to that problem, I suggested the most simple solution: pass a setter function all the way to the inner cat:
const PageLayout =( ) => { const [buttons,setButtons] = React.useState([]); return ( <div> <Buttons buttons={buttons} /> <h1>Title</h1> <SetCat setButtons={setButtons} /> </div> ) } const SetCat = ({setButtons}) => { switch ("fatCat"){ case "smallCat": return <SmallCat setButtons={setButtons} /> case "fatCat": return <FatCat setButtons={setButtons} /> default: return <CatsList setButtons={setButtons} /> } } const FatCat = ({ setButtons }) => { const buttons = ['Weight issues', 'Close', 'Export']; React.useEffect(()=>{ setButtons(buttons); },[]) return <img src="http..." /> }
“You are breaking React” cried someone, “You cant set a high element by sending a setter inside”.
“Yes”, I replied “It’s an ugly constraint, but it’s the most simple solution to the problem”.
“But you are piping an additional prop”, he continued to argue, suggesting a different approach…
Solution II: JSX Restructure solution
His solution was to reconstruct the JSX, so the buttons will be included inside the selected cat element:
const App = () => { return <SetCat />; } const FatCat = ({ }) => { const buttons = ['Weight issues', 'Close', 'Export']; const component = <img src="http..." /> return <PageLayout component={component} buttons={buttons} /> } const PageLayout =({component,buttons}) => { return ( <div> <Buttons buttons={buttons} /> <h1>Title</h1> { component} </div> ) }
“No way”, Someone announced shocked, “You are breaking React” he reuses the same words, “You define a global element, within a lower and more specific one”. “And it’s not elegant”, I added some useless criticism.
Since he did not see the problem, I rephrased the problem: “You can’t allow an inner element to be responsible for the global context”. “The fat cat can return an element without the page layout, and break everything”, I tried once more.
“Don’t you see the code smell: the { component } wildcard? It breaks every TypeScript concept you adore so much, I mocked.
“O.K.”, he replied with an uncertain voice. “But your solution is also a bad practice”, he announced. I agreed but insisted that the overall code is much clearer, constructed better, and more restricted.
Solution III: Use context solution
“We can use useContect”, he declared. “This way we don’t need to pipe the setter, and have a more organized codebase”.
const ButtonsContext = React.createContext() const App = () => { const [buttons,setButtons] = React.useState([]); const buttonsContext = {buttons,setButtons} return ( <ButtonsContext.Provider value={buttonsContext}> <PageLayout /> </ButtonsContext.Provider>) } const Buttons = ({ }) => { const {buttons} = React.useContext(ButtonsContext); return <div className="modal"> {buttons?.map(b => <button key={b}>{b}</button>)} </div>; } const FatCat = ({ }) => { const {setButtons} = React.useContext(ButtonsContext); const buttons = ['Weight issues', 'Close', 'Export']; React.useEffect(()=>{ setButtons(buttons); },[]); return <img src="http..." /> }
“Yes, but…”, I hesitated, “My intuition gives me a bad feeling”. There was a silent moment.
“I can’t defend my intuition, that’s why It’s called intuition. But as far as I remember context is not meant to pass props, but for global objects”. Obviously, he disagreed. So I revisited the official doc and created appendix A (The last section on this post). But before there was an option to display the research conclusions, the quite smartass member announced “But for that there is Portal”.
Solution IV: Use portal
React Portal allows to render an element on other DOM elements without affecting the React hierarchy. In other words, it’s exactly what we were looking for: a way to inject some HTML to a different location:
const PageLayout = () => { return ( <div> <div id='buttons' /> <h1>Title</h1> <SetCat /> </div> ) } const ButtonsPortal = ({ buttons }) => { let [res, setRes] = React.useState(null); React.useLayoutEffect(() => { const portal = ReactDOM.createPortal( <Buttons buttons={buttons} />, document.querySelector('#buttons')); setRes(portal); }, []); return res; } const FatCat = ({ }) => { const buttons = ['Weight issues', 'Close', 'Export']; return <> <img src="http..." /> <ButtonsPortal buttons={buttons} /> </> }
Conclusion:
with good team culture, sometimes there are health interesting arguments and discussions. A perfect opportunity to challenge, and be changed, to learn and to sharpen your and the members’ knowledge. And I’m grateful for each and every member of my team. And by the way, I was given the opportunity to recall Portal.
…
…
…
…
Appendix A: useContext bad practice
React context: Before You Use Context
Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult
If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context.
React Context: Before You Use Context
…
However, sometimes the same data needs to be accessible by many components in the tree, and at different nesting levels. Context lets you “broadcast” such data, and changes to it, to all components below.
Pitfalls of overusing React Context
But if you have static data that undergoes lower-frequency updates such as preferred language, time changes, location changes, and user authentication, passing down props with React Context may be the best option.
Pitfalls of overusing React Context, Ibrahima Ndaw
…
React Context is an excellent API for simple apps with infrequent state changes, but it can quickly devolve into a developer’s nightmare if you overuse it for more complex projects.
This is how to use the react context API with hooks for a clean code architecture
That being said, the context, just like everything in the world of programming, is just a tool. Don’t abuse it! I must bold this one.
Do not abuse the react context
If you find yourself in a need to share state between unrelated components — extract the local state to a context and see how it goes. Let it evolve.
Don’t go into extremes, because they are always worse than every other more balanced solution.
Borislav Grigorov
How to use React Context effectively:
You shouldn’t be reaching for context to solve every state sharing problem that crosses your desk.
Kent C. Dodds
Does the React Context API replace the need for component props?
There is no other reason. If I want to reuse component or export it that depends context, user has no idea about it. Use cases like logged User, theme, or sone global state like work flow detail for the whole application etc, makes a good use case.
Karthik R on stackoverflow
Don’t over-use useContext: props are still a thing
A simple and applicable rule seems to be to use useContext on components preferably high up in the tree, and rely more on passing props than on context. This rule is not new, but will be even more important because of the simple application of context.
STEFAN T. OERTEL’S BLOG