Is it ok to define nested component with React Hooks? - javascript

I am using Hooks + Render Props in my Preact/React application. Instead of passing simple state to the render(), I am passing a component function that can be used by the user of this component.
Also, my component is Hybrid in a sense that that SortDropdown is not UI-Less like we typically have with Render Prop components. It provides some basic UI in the form of anchor element which triggers the actual drop down and shows the custom content via Render Props render function. This is how I have implemented it:
export function SortDropdown(props: SortDropdownProps) {
const { render, onSelect, value } = props;
const anchor = (ref: Ref<Element>, open: VoidFunction) => (
<SortButton buttonElm={ref} onClick={open} />
);
const surfaceContent = (ref: Ref<any>, select: SortSelectFn) => {
// NESTED INNER COMPONENT - IS IT RIGHT?
function SortMenuItem(props: SortMenuItemProps) {
const { children, field } = props;
// THE HOOK IN QUESTION
const [someState, setSomeState] = useState(false);
const isSelected = value.key === field.key;
const onClick = () => select(field);
return (
<SortMenuButton canTrigger={someState} selected={someState} onClick={onClick}>
{children}
</SortMenuButton>
);
}
return (
<div ref={ref}>{render(SortMenuItem, select)}</div>
);
};
return (
<BaseDropdown anchor={anchor} surface={surfaceContent} onSelect={onSelect as any} />
);
}
Here I have two questions. First, does defining an inner nested component, here - SortMenuItem which is then passed to the render function violates the rules of Hooks? The component may be called n-times or may not be used at all by the calling component.
Second, is it a right practice to define a nested Higher-Order Component, here - SortMenuItem being an abstraction over SortMenuButton?

Related

React Hook is called in function that is neither a React function component nor a custom React Hook function

I have this following ESLint warning :
React Hook "useBuilderFeatureFlagContext" is called in function "Slide.RenderBuilder" that is neither a React function component nor a custom React Hook function.
and this is the following component :
Slide.RenderBuilder = ({ children }) => {
const flags = useBuilderFeatureFlagContext();
return (
<>
<SlideWrapper $flags={flags}>
{children}
</SlideWrapper>
<ImageSetter attributeName="containerBackgroundImage" />
</>
);
};
How do I write a rule that can whitelist this specific case ?
If you can, define the component first, then add it to your object.
const RenderBuilder = ({ children }) => {
const flags = useBuilderFeatureFlagContext();
return (/**/);
};
Slide.RenderBuilder = RenderBuilder;
That way, the rule properly check for hooks, and you have the structure you're looking for.
Make sure to use it as a component <Slide.RenderBuilder /> otherwise you might end up breaking rules of hooks.

What is the reason behind react component showing the changes of state inside them?

When we have many components in react project and sometimes we use multiple pre-made components for making a page. While using onChange inside a component and showing the result of the state, in this case, what functionality of components allows the value render of state and how it works when we have multiple components inside other components.
Here is an Ex...
function Component() {
const [value, setValue] = React.useState()
const handleChange = val => {
setValue(val)
}
return (
<React.Fragment>
<Compo1 //perform adding +1
onChange={handleChange}
/>
Value: {value} // 1
{console.log("value", value)} // showing right value
<Compo2>
<Compo3>
<Compo1 //perform adding +1
onChange={handleChange}
/>
Value:{value} // undefined
{console.log("value", value)} // showing right value
</Compo3>
{console.log("value", value)} // showing right value
</Compo2>
</React.Fragment>
)
}
render(<Component />)
In this case why console is showing the right value but the state variable value is showing undefined.
The only way I can get that code to do what you say it does is when you incorrectly use React.memo on Compo3:
const Compo1 = ({ onChange }) => (
<button onClick={() => onChange(Date.now())}>+</button>
);
const Compo2 = ({ children }) => <div>{children}</div>;
const Compo3 = React.memo(
function Compo3({ children }) {
return <div>{children}</div>;
},
() => true//never re render unless you re mount
);
function Component() {
const [value, setValue] = React.useState(88);
const handleChange = React.useCallback(() => {
setValue((val) => val + 1);
}, []);
return (
<React.Fragment>
<Compo1 //perform adding +1
onChange={handleChange}
/>
works: {value}-----
<Compo2>
<Compo3>
<Compo1 //perform adding +1
onChange={handleChange}
/>
broken:{value}-----
</Compo3>
</Compo2>
</React.Fragment>
);
}
ReactDOM.render(
<Component />,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Maybe you can do the same if you do some wacky stuff with shouldComponentUpdate
A component will render when:
When the parent renders the child and the child is a functional component (not wrapped in React.memo)
When the parent renders the child with different prop values than the previous render.
When value in [value,setValue]=useState() or when this.state changes (when state changes).
When someContext in value = useContext(someContext) changes (even if value doesn't change).
In most cases when value in value = useCustomHoom() changes but this is not guaranteed for every hook.
When Parent renders and passes a different key prop to Child than the previous render (see 2). This causes the Child to unmount and re mount as well.
In the example the Compo3 wants to re render because Parent is re rendered due to a state change and passes different props (props.children).
Compo3 is not a functional component because it's wrapped in React.memo. This means that Compo3 will only re render if props changed (pure component).
The function passed as the second argument to React.memo can be used to custom compare previous props to current props, if that function returns true then that tells React the props changed and if it returns false then that tells React the props didn't change.
The function always returns true so React is never told that the props changed.

React optimize functional components

I have two React functional components Parent and Node. Parent renders list of Node components and passes click handler to each Node. When I click on the Node, in the Parent the new state is set and all components rerenders.
const Parent = (props) => {
const [clickedList, setClickedList] = useState([]);
const onNodeClick = (node) => {
let newList = [...clickedList, node];
setClickedList(newList);
}
return (
<div>
{someNodes.map(node => <Node key={node.id} node={node} onNodeClick={props.onNodeClick}/>)}
</div>
)
}
const Node = (props) => {
const handleClick = (node) => {
props.onNodeClick(node);
}
return (
<div onClick={() => handleClick(node)}>
{props.node.name}
</div>
)
}
The list of nodes contains more than 400 items. Rerendering of each component takes time. I tried to use React.memo for Node component, but the feature is that I pass the function to Node, which has different references after each rerendering of the Parent and React.memo component rerenders too. Also I tried to provide a custom comparison function for React.memo where I skip comparsion of passed function. The result is components do not rerender, but component state clickedList does not change in the click handler in the Parent.
I also tried to use useCallback hook on the click handler in the Parent, the result is similar. If I give no dependencies [] then component state clickedList does not change.
const memorizedNodeClick = useCallback(
(node) => {
onNodeClick(node);
}, [],
);
If I give state in dependencies [clickedList] then all more than 400 components rerenders
const memorizedNodeClick = useCallback(
(node) => {
onNodeClick(node);
}, [clickedList],
);
How can I prevent unnecessary component rendering and keep all clicked items in the clickedList state?

Using react hooks inside render prop function

Taken from Rules of Hooks:
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function
Given this recommendation by the react team, would using a hook at the top of a render props function be inadvisable? Assuming it depends on the value returned by the function.
const MyComponent = () => (
<RenderPropComponent>
{value => {
React.useEffect(() => {
// Magic
}, [value]);
return <p>Hello</p>;
}}
</RenderPropComponent>
);
I don't feel like it breaks their requirement
By following this rule, you ensure that Hooks are called in the same order each time a component renders
But should I be doing the following instead?
const MyComponent = ({ value }) => {
React.useEffect(() => {
// Magic
}, [value]);
return <p>Hello</p>;
};
const MyContainer = () => (
<RenderPropComponent>
{value => <MyComponent value={value} />}
</RenderPropComponent>
);
Hooks keep track of current rendering element. And render prop function is not an element. So you will hook into calling element and not into your prop function. This render prop function will be treated as a custom hook. You will get unexpected behavior if RenderPropComponent calls render prop function conditionally.
This does not break the rules of hooks - https://unsplash.com/blog/calling-react-hooks-conditionally-dynamically-using-render-props/#waitdoesntthisbreaktherulesofhooks

How to force a functional React component to render?

I have a function component, and I want to force it to re-render.
How can I do so?
Since there's no instance this, I cannot call this.forceUpdate().
πŸŽ‰ You can now, using React hooks
Using react hooks, you can now call useState() in your function component.
useState() will return an array of 2 things:
A value, representing the current state.
Its setter. Use it to update the value.
Updating the value by its setter will force your function component to re-render,
just like forceUpdate does:
import React, { useState } from 'react';
//create your forceUpdate hook
function useForceUpdate(){
const [value, setValue] = useState(0); // integer state
return () => setValue(value => value + 1); // update state to force render
// A function that increment πŸ‘†πŸ» the previous state like here
// is better than directly setting `setValue(value + 1)`
}
function MyComponent() {
// call your hook here
const forceUpdate = useForceUpdate();
return (
<div>
{/*Clicking on the button will force to re-render like force update does */}
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
You can find a demo here.
The component above uses a custom hook function (useForceUpdate) which uses the react state hook useState. It increments the component's state's value and thus tells React to re-render the component.
EDIT
In an old version of this answer, the snippet used a boolean value, and toggled it in forceUpdate(). Now that I've edited my answer, the snippet use a number rather than a boolean.
Why ? (you would ask me)
Because once it happened to me that my forceUpdate() was called twice subsequently from 2 different events, and thus it was reseting the boolean value at its original state, and the component never rendered.
This is because in the useState's setter (setValue here), React compare the previous state with the new one, and render only if the state is different.
Update react v16.8 (16 Feb 2019 realease)
Since react 16.8 released with hooks, function components have the ability to hold persistent state. With that ability you can now mimic a forceUpdate:
function App() {
const [, updateState] = React.useState();
const forceUpdate = React.useCallback(() => updateState({}), []);
console.log("render");
return (
<div>
<button onClick={forceUpdate}>Force Render</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"/>
Note that this approach should be re-considered and in most cases when you need to force an update you probably doing something wrong.
Before react 16.8.0
No you can't, State-Less function components are just normal functions that returns jsx, you don't have any access to the React life cycle methods as you are not extending from the React.Component.
Think of function-component as the render method part of the class components.
Official FAQ now recommends this way if you really need to do it:
const [ignored, forceUpdate] = useReducer(x => x + 1, 0);
function handleClick() {
forceUpdate();
}
Simplest way πŸ‘Œ
if you want to force a re-render, add a dummy state you can change to initiate a re-render.
const [rerender, setRerender] = useState(false);
...
setRerender(!rerender); //whenever you want to re-render
And this will ensure a re-render, And you can call setRerender(!rerender) anywhere, whenever you want :)
I used a third party library called
use-force-update
to force render my react functional components. Worked like charm.
Just use import the package in your project and use like this.
import useForceUpdate from 'use-force-update';
const MyButton = () => {
const forceUpdate = useForceUpdate();
const handleClick = () => {
alert('I will re-render now.');
forceUpdate();
};
return <button onClick={handleClick} />;
};
Best approach - no excess variables re-created on each render:
const forceUpdateReducer = (i) => i + 1
export const useForceUpdate = () => {
const [, forceUpdate] = useReducer(forceUpdateReducer, 0)
return forceUpdate
}
Usage:
const forceUpdate = useForceUpdate()
forceUpdate()
If you already have a state inside the function component and you don't want to alter it and requires a re-render you could fake a state update which will, in turn, re-render the component
const [items,setItems] = useState({
name:'Your Name',
status: 'Idle'
})
const reRender = () =>{
setItems((state) => [...state])
}
this will keep the state as it was and will make react into thinking the state has been updated
This can be done without explicitly using hooks provided you add a prop to your component and a state to the stateless component's parent component:
const ParentComponent = props => {
const [updateNow, setUpdateNow] = useState(true)
const updateFunc = () => {
setUpdateNow(!updateNow)
}
const MyComponent = props => {
return (<div> .... </div>)
}
const MyButtonComponent = props => {
return (<div> <input type="button" onClick={props.updateFunc} />.... </div>)
}
return (
<div>
<MyComponent updateMe={updateNow} />
<MyButtonComponent updateFunc={updateFunc}/>
</div>
)
}
The accepted answer is good.
Just to make it easier to understand.
Example component:
export default function MyComponent(props) {
const [updateView, setUpdateView] = useState(0);
return (
<>
<span style={{ display: "none" }}>{updateView}</span>
</>
);
}
To force re-rendering call the code below:
setUpdateView((updateView) => ++updateView);
None of these gave me a satisfactory answer so in the end I got what I wanted with the key prop, useRef and some random id generator like shortid.
Basically, I wanted some chat application to play itself out the first time someone opens the app. So, I needed full control over when and what the answers are updated with the ease of async await.
Example code:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// ... your JSX functional component, import shortid somewhere
const [render, rerender] = useState(shortid.generate())
const messageList = useRef([
new Message({id: 1, message: "Hi, let's get started!"})
])
useEffect(()=>{
async function _ () {
await sleep(500)
messageList.current.push(new Message({id: 1, message: "What's your name?"}))
// ... more stuff
// now trigger the update
rerender(shortid.generate())
}
_()
}, [])
// only the component with the right render key will update itself, the others will stay as is and won't rerender.
return <div key={render}>{messageList.current}</div>
In fact this also allowed me to roll something like a chat message with a rolling .
const waitChat = async (ms) => {
let text = "."
for (let i = 0; i < ms; i += 200) {
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
messageList.current.push(new Message({
id: 100,
message: text
}))
if (text.length === 3) {
text = "."
} else {
text += "."
}
rerender(shortid.generate())
await sleep(200)
}
if (messageList.current[messageList.current.length - 1].id === 100) {
messageList.current = messageList.current.filter(({id}) => id !== 100)
}
}
If you are using functional components with version < 16.8. One workaround would be to directly call the same function like
import React from 'react';
function MyComponent() {
const forceUpdate = MyComponent();
return (
<div>
<button onClick={forceUpdate}>
Click to re-render
</button>
</div>
);
}
But this will break if you were passing some prop to it. In my case i just passed the same props which I received to rerender function.
For me just updating the state didn't work. I am using a library with components and it looks like I can't force the component to update.
My approach is extending the ones above with conditional rendering. In my case, I want to resize my component when a value is changed.
//hook to force updating the component on specific change
const useUpdateOnChange = (change: unknown): boolean => {
const [update, setUpdate] = useState(false);
useEffect(() => {
setUpdate(!update);
}, [change]);
useEffect(() => {
if (!update) setUpdate(true);
}, [update]);
return update;
};
const MyComponent = () => {
const [myState, setMyState] = useState();
const update = useUpdateOnChange(myState);
...
return (
<div>
... ...
{update && <LibraryComponent />}
</div>
);
};
You need to pass the value you want to track for change. The hook returns boolean which should be used for conditional rendering.
When the change value triggers the useEffect update goes to false which hides the component. After that the second useEffect is triggered and update goes true which makes the component visible again and this results in updating (resizing in my case).

Categories

Resources