Using React Hooks in in an IIFE breaks rules of hooks - javascript

Background
I'm working on a sidebar component that can be opened from many buttons in the UI. I want to 1) only render it once, 2) grant access to update its isVisible state to these buttons without drilling props down through a common ancestor.
Expectations vs Reality
I'd expect that I could create a context with its own api method to update an internal state. In my code sample I'm attempting to do this with an IIFE.
Questions
How does this break the rules of hooks?
How else can I provide an update function to this context?
export const SidebarContext = createContext((() => {
const [isVisible, setIsVisible] = useState(false)
return {
isVisible,
toggleVisibility: () => setIsVisible(!isVisible)
}
})())

createContext() receives the default value. So you're defining a function which is invoked immediately and the result of it will be used as default value for the context. That's where the useState breaks this rule:
Call Hooks from React function components.
In order to accomplish what you want you can do this:
import React, { createContext, useContext, useState } from "react";
const SidebarContext = createContext();
function Provider({ children }) {
let [isVisible, setIsVisible] = useState(false);
let toggle = useCallback(() => setIsVisible(s => !s), [setIsVisible])
// Pass the `state` and `functions` to the context value
return (
<SidebarContext.Provider value={{ isVisible, toggle }}>
{children}
</SidebarContext.Provider>
);
}
function YourButton() {
let { isVisible, toggle } = useContext(SidebarContext);
return (
<div>
<div>Sidebar is {isVisible : 'open': 'close'}</div>
<button onClick={toggle}>
Toggle
</button>
</div>
);
}
function App() {
return (
<Provider>
<YourButton />
</Provider>
);
}

Related

How state Varibales in React JS works without using it on UI

Why my react functional component is rendered only 2 times when I click "Click me" button more than two times, if I have used a state variable and a change to that state variable inside that component, "but haven't used that state variable anywhere inside the UI"?
code:
const Header = () => {
const [title, setTitle] = useState("My Title");
console.log('rendered');
return (
<>
<button onClick={e => {
setTitle("Title My");
}}>Click me</button>
</>
);
};
export default Header;
In this case, the component is only rendered two times because React is optimized for performance, and it will only re-render a component if its state or props have changed.
If you want the component to re-render each time the button is clicked, you have to set the state with a different value, not always the same (Title my in your case)
I suspect React doesn't see any reason to re-render because state hasn't changed; the value is the same as the last time. But if you want to re-render you can forceRender using the following code.
import { useState } from 'react'
function useForceUpdate(){
const [value, setValue] = useState(0);
return () => setValue(value => value + 1);
}
const Header = () => {
const forceUpdate = useForceUpdate();
const [title, setTitle] = useState("My Title");
console.log('rendered');
return (
<>
<button onClick={e => {
setTitle("Title My");
forceUpdate();
}}>Click me</button>
</>
);
};
export default Header;

React hooks: Dynamically mapped component children and state independent from parent

I am gathering posts (called latestFeed) from my backend with an API call. These posts are all mapped to components and have comments. The comments need to be opened and closed independently of each other. I'm governing this mechanic by assigning a piece of state called showComment to each comment. showComment is generated at the parent level as dictated by the Rules of Hooks.
Here is the parent component.
import React, { useState, useEffect } from "react";
import { getLatestFeed } from "../services/axios";
import Child from "./Child";
const Parent= () => {
const [latestFeed, setLatestFeed] = useState("loading");
const [showComment, setShowComment] = useState(false);
useEffect(async () => {
const newLatestFeed = await getLatestFeed(page);
setLatestFeed(newLatestFeed);
}, []);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
return (
<div className="dashboardWrapper">
<Child posts={latestFeed} showComment={showComment} handleComment={handleComment} />
</div>
);
};
export default Parent;
latestFeed is constructed along with showComment. After latestFeed comes back with an array of posts in the useEffect hook, it is passed to the child show here:
import React, { useState } from "react";
const RenderText = ({ post, showComment, handleComment }) => {
return (
<div key={post._id} className="postWrapper">
<p>{post.title}</p>
<p>{post.body}</p>
<Comments id={post._id} showComment={showComment} handleComment={() => handleComment(post)} />
</div>
);
};
const Child = ({ posts, showComment, handleComment }) => {
return (
<div>
{posts.map((post) => {
<RenderPosts posts={posts} showComment={showComment} handleComment={handleComment} />;
})}
</div>
);
};
export default Child;
However, whenever I trigger handleComments, all comments open for all posts. I'd like them to be only the comment that was clicked.
Thanks!
You're attempting to use a single state where you claim you want multiple independent states. Define the state directly where you need it.
In order to do that, remove
const [showComment, setShowComment] = useState(false);
const handleComment = () => {
showComment ? setShowComment(false) : setShowComment(true);
};
from Parent, remove the showComment and handleComment props from Child and RenderText, then add
const [showComment, handleComment] = useReducer(state => !state, false);
to RenderText.

Dynamic import of react hooks

Can we dynamically import hooks based on the value passed to the component?
For eg.
App.js
<BaseComponent isActive />
BaseComponent.js
if(props.isActive) {
// import useActive (custom Hook)
}
I donot want these(custom hook) files to be imported and increase the size of BaseComponent even when the props contain falsey values.
You can dynamically import hooks as it is just a function (using require), but you shouldn't because you can't use hooks inside conditions.
See Rules of Hooks
Only call Hooks at the top level. Don’t call Hooks inside loops, conditions, or nested functions.
If you want conditionally use a hook, use the condition in its implementation (look for example at skip option of useQuery hook from Apollo GraphQL Client).
const useActive = (isUsed) => {
if (isUsed) {
// logic
}
}
You should extract the logic inside the useActive hook and dynamically import it instead of dynamically importing the hook since you should not call Hooks inside loops, conditions, or nested functions., checkout Rules of Hooks:
Let's say your useActive hook was trying to update the document title (in real world, it has to be a big chunk of code that you would consider using dynamic import)
It might be implemented as below:
// useActive.js
import { useEffect } from "react";
export function useActive() {
useEffect(() => {
document.title = "(Active) Hello World!";
}, []);
}
And you tried to use it in the BaseComponent:
// BaseComponent.js
function BaseComponent({ isActive }) {
if (isActive) { // <-- call Hooks inside conditions ❌
import("./useActive").then(({ useActive }) => {
useActive();
});
}
return <p>Base</p>;
}
Here you violated the rule "don't call Hooks inside conditions" and will get an Invalid hook call. error.
So instead of dynamically import the hook, you can extract the logic inside the hook and dynamically import it:
// updateTitle.js
export function updateTitle() {
document.title = "(Active) Hello World!"
}
And you do the isActive check inside the hook:
// BaseComponent.js
function BaseComponent({ isActive }) {
useEffect(() => {
if (!isActive) return;
import("./updateTitle").then(({ updateTitle }) => {
updateTitle();
});
}, [isActive]);
return <p>Base</p>;
}
It works fine without violating any rules of hooks.
I have attached a CodeSandbox for you to play around:
You could create a Higher Order Component that fetches the hook and then passes it down as a prop to a wrapped component. By doing so the wrapped component can use the hook without breaking the rules of hooks, eg from the wrapped component's point of view, the reference to the hook never changes and the hook gets called everytime the wrapped component renders. Here is what the code would look like:
export function withDynamicHook(hookName, importFunc, Component) {
return (props) => {
const [hook, setHook] = useState();
useEffect(() => {
importFunc().then((mod) => setHook(() => mod[hookName]));
}, []);
if (!hook) {
return null;
}
const newProps = { ...props, [hookName]: hook };
return <Component {...newProps} />;
};
}
// example of a Component using it:
const MyComponent = ({useMyHook}) => {
let something = useMyHook();
console.log(something)
return <div>myHook returned something, see the console to inspect it </div>
}
const MyComponentWithHook = withDynamicHook('useMyHook', () => import('module-containing-usemyhook'), MyComponent)
To whoever encountered it as well: You can't use Hooks inside dynamically imported components(however, apparently if you does not use hooks even the first example works):
instead of:
const useDynamicDemoImport = (name) => {
const [comp, setComp] = useState(null);
useEffect(() => {
let resolvedComp = false;
import(`#site/src/demos/${name}`)
.then((m) => {
if (!resolvedComp) {
resolvedComp = true;
setComp(m.default);
}
})
.catch(console.error);
return () => {
resolvedComp = true;
};
}, []);
return comp;
};
const DemoPreviewer: FC<DemoPreviewerProps> = (props) => {
comp = useDynamicDemoImport(props.name);
return (
<Paper sx={{ position: "relative" }}>
{comp}
</Paper>
);
};
export default DemoPreviewer
use React Lazy instead and render the component later
const useDynamicDemoImport = (name) => {
const Comp = React.lazy(() => import(`#site/src/demos/${name}`));
return comp;
};
const RootDemoPreviewer: FC<DemoPreviewerProps> = (props) => {
console.log("RootDemoPreviewer");
return (
<React.Suspense fallback={<div>Loading...</div>}>
<DemoPreviewer {...props} />
</React.Suspense>
);
};
const DemoPreviewer: FC<DemoPreviewerProps> = (props) => {
const Comp = useDynamicDemoImport(props.name);
return (
<Paper sx={{ position: "relative" }}>
<Comp />
</Paper>
);
};
export default RootDemoPreviewer

hook change state doesn't update context provider's value?

I have 2 component, and a context provider, when I call my hook at parent level, I have no issue changing the state and having those 2 component getting the value via context
working demo of contex api usage but I call change state at parent level which is not what I wanted
https://stackblitz.com/edit/react-51e2ky?file=index.js
I want to change state at inner component with hook, but I don't see the value been changed when I click on the navbar login.
https://stackblitz.com/edit/react-rgenmi?file=Navbar.js
parent:
const App = () => {
const {user} = loginHook()
return (
<UserContext.Provider value={user}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
Navbar.js
const Navbar = () => {
const user = React.useContext(userContex)
const {setUser} = loginHook()
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
custom hook
const loginHook = () => {
const [user, setUser] = React.useState(null)
return {
user,
setUser
}
}
I can pass setUser from parent to children but I want to avoid that, I expect I can use context api and react hook seamlessly.
Currently, you're only setting the user value in the context, which is why getting the correct value will work.
However, in your Navbar.js component, you are making a call to loginHook, which will create a new "instance" of that hook, effectively having its own state.
I suggest you add the update function in your context as well, as such
const App = () => {
const {user, setUser} = loginHook()
return (
<UserContext.Provider value={{ user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
That way you can access the setUser in your children as well, e.g.
const Navbar = () => {
const {user, setUser} = React.useContext(userContex)
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
Also, small note: it's best to start you custom hook with use, as that's a best-practice when writing your own hooks.
Important caveat however, this is not really a good practice. If your user were to change, all components that are only listening to setUser will also get an update an thus do a useless rerender. You can solve this by using two different contexts, one for the value, and one for the updater. You can read more about this here
You cannot change the parent's context information from the child, no. You'll need to pass something to the child from the parent that the child can use to let the parent know that the context needs to be updated (such as the parent's copy of setUser). You can do that via a prop or by adding setUser to the context, though I'd lean toward just doing it as a prop to components that need to be able to set the user, rather than context they all have access to.
The reason using loginHook in both places didn't work is that each component (App and Navbar) has its own copy of user. This is fundamental to how hooks work. (If it somehow made them share the state information, useState wouldn't work at all — all state would be shared across all components.) Dan Abramov's A Complete Guide to useEffect may be a helpful read (it's more about how hooks and components work than it is specifically about useEffect).
You must note that custom hooks do not share instance references, so if you use the loginHook in App and another one in Navbar, they will create 2 separate states and updaters
Now using a setter from custom hook will now update the state in context.
You can restructure this by writing your loginHook so that it internally uses context and then using it
const App = () => {
const [user, setUser] = useState();
return (
<UserContext.Provider value={{user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
const Navbar = () => {
const {user, setUser} = loginHook();
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
const loginHook = () => {
const {user, setUser} = React.useContext(UserContext)
return {
user,
setUser
}
}
Now there are multiple ways to write this code, However the best way in the above scenario is not use a custom hook at all since it anyway is not useful
const App = () => {
const [user, setUser] = useState();
return (
<UserContext.Provider value={{user, setUser}}>
<Navbar />
<Content />
</UserContext.Provider>
);
}
const Navbar = () => {
const {user, setUser} = React.useContext(UserContext);
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}
In your Navbar.js you use your loginHook hook which will create a new separate state that is different from the state used in your App.js. You need to write your hook so that is uses the context instead of useState:
/* UserContext.js */
const UserContext = createContext();
export const UserProvider = ({children}) => {
const [user, setUser] = useState(null);
return (
<UserContext.Provider value={{user, setUser}}>
{children}
</UserContext.Provider>
);
}
export const useLogin = () => useContext(UserContext);
Then use it like that:
/* App.js */
import {UserProvider} from './UserContext';
const App = () => (
<UserProvider>
<Navbar />
<Content />
</UserProvider>
);
and
/* Navbar.js */
import {useLogin} from './UserContext';
const Navbar = () => {
const {user, setUser} = useLogin();
return <div>{user ? <span>{user.name}</span> : <button onClick={() => {
setUser({
name: 'jane'
})
}}>navbar Login</button>}</div>
}

Questions about useCallback hook and anonymous function

When passing a callback function, especially when passing a parameterized function, I know that I should use the useCallback hook because the use of anonymous functions can adversely affect performance.
the example of anonymous function I said is like this.
import React, { useState } from 'react';
const Component = () => {
const [param, setParam] = useState('');
...
return (
...
<SomeComponent
onClick={() => setParam('parameter')}
{...others}
/>
);
}
In the process of converting an anonymous function to use this hook, I encountered an error saying 'Too many renders' or it didn't work properly.
But I don't know exactly in what situation and in what situation.
and I used useCallback like below.
import React, { useState, useCallback } from 'react';
const Component = () => {
const [param, setParam] = useState('');
const handleClick = useCallback((params) => {
setParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
);
}
However, when using an anonymous function to return within useCallback, it also worked.
This means code like here. (Only the differences compared to the code above.)
const handleClick = useCallback((params) => {
return () => setParam(params);
},[]);
In this situation, I wonder if it's worse than just using an anonymous function inside the useCallback if I simply use an anonymous function instead of using this hook.
const handleClick = useCallback((params) => {
setParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
);
in the above code during first render, at this statement "onClick={handleClick('parameter')}" handleClick function is called with a string named "parameter". since handleClick has setParam("parameter"), it will update state. updating state will cause re-render which will again come to same statement "onClick={handleClick('parameter')}" causing infinte loop.
following code you added later works because you are not updating state, instead returning a function, which acts as onclick handler.
const handleClick = useCallback((params) => {
return () => setParam(params);
},[]);
better way of doing this shoud be as follwing,
import React, { useState, useCallback } from 'react';
const Component = () => {
const [param, setParam] = useState('');
const handleClick = useCallback((params) => {
setParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick}
{...others}
/>
);
}
coming back to your question , comparing performance depends on the other function definitions and render times of child compoents inside return function inside your Compoenent.
let's say you have another onclickHanldier named 'anotherHandleClick' inside your app.
then your component looks like this
const Component = () => {
const [param, setParam] = useState('');
const [anotherParam, setAnotherParam] = useState('');
const handleClick = (params) => {
setParam(params);
};
const anotherHandleClick =(params) => {
setAnotherParam(params);
};
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
<SomeComponent
onClick={antherHandleClick('parameter')}
{...others}
/>
);
}
in the above component when any one of "SomeCompoenent" clicked entiere "Component" re-renders, so the handler functions are newly defined.and when both 's does referential equality check on onclick handler functions, they believe it is new handler function which casues them to render both.
in that case it is better to use useCallBack hook as following,
const Component = () => {
const [param, setParam] = useState('');
const [anotherParam, setAnotherParam] = useState('');
const handleClick = useCallback((params) => {
setParam(params);
},[]);
const anotherHandleClick = useCallback((params) => {
setAnotherParam(params);
},[]);
...
return (
...
<SomeComponent
onClick={handleClick('parameter')}
{...others}
/>
<SomeComponent
onClick={antherHandleClick('parameter')}
{...others}
/>
);
}
in the above code when any one is clicked , state changes. then when rendering, useCallback make sure that onclick handler refernces didn't change. so having dependency of onclick handlers won't be rerendered.
so final thought is It's creating a function on each render in both cases. the second (because it'e wrapped in a useCallback) will return the memoized function created on the initial render
when to use useMemo or useCallback refer this
In your code:
const handleClick = useCallback((params) => {
setParam(params);
},[]);
You should pass the parameters into the second argment of useCallback like so:
const handleClick = useCallback((params) => {
setParam(params);
},[setParam,param, SETPARAMACTUALVALUE]);
// Change SETPARAMACTUALVALUE to whatever the variable name is for the `setParam` hook
useCallback hook usage will be better if you want to save the function until hook's dependency changed. It gives you better performance because the hook remember internal function.

Categories

Resources