Let's say I want to create a UI component for an "accordion" (a set of collapsible panels). The parent component controls the state of which panels are open, while the child panels should be able to read the context to determine whether or not they're open.
const Accordion = ({ children }) => {
const [openSections, setOpenSections] = useState({})
const isOpen = sectionId => Boolean(openSections[sectionId])
const onToggle = sectionId => () =>
setOpenSections({ ...openSections, [sectionId]: !openSections[sectionId] })
const context = useMemo(() => createContext(), [])
// Can't tell children to use *this* context
return (
<context.Provider value={useMemo(() => ({ isOpen, onToggle }), [isOpen, onToggle])}>
{children}
</context.Provider>
)
}
const AccordionSection = ({ sectionId, title, children }) => {
const { isOpen, onToggle } = useContext(context)
// No way to infer the right context
return (
<>
<button onClick={onToggle(sectionId)}>{isOpen(sectionId) ? 'Close' : 'Open'}</button>
{isOpen && children}
</>
)
}
The only way I could think of accomplishing this would be to have Accordion run an effect whenever children changes, then traverse children deeply and find AccordionSection components, while not recursing any nested Accordion components -- then cloneElement() and inject context as a prop to each AccordionSection.
This seems not only inefficient, but I'm not even entirely sure it will work. It depends on children being fully hydrated when the effect runs, which I'm not sure if that happens, and it also requires that Accordion's renderer gets called whenever deep children change, which I'm not sure of either.
My current method is to create a custom hook for the developer implementing the Accordion. The hook returns a function which returns the isOpen and onToggle functions which have to manually be passed to each rendered AccordionSection. It works and is possibly more elegant than the children solution, but requires more overhead as the developer needs to use a hook just to maintain what would otherwise be state encapsulated in Accordion.
React.createContext will return an object that holds 2 components:
Provider
Consumer
These 2 components can share data, the Consumer can "grab" the context data from the nearest Provider up the tree (or use the useContext hook instead of rendering a Consumer).
You should create the context object outside the parent component and use it to render a Consumer inside your children components (or use the useContext hook).
Simple example:
const myContext = createContext();
const Accordion = ({ children }) => {
// ...
return (
<myContext.Provider value={...} >
{children}
</myContext.Provider>
)
}
const AccordionSection = (...) => {
const contextData = useContext(myContext);
// use the data of your context as you wish
// ...
}
Note that i used the useContext hook instead of rendering the Consumer, its up to you if you want to use the hook or the Consumer.
You can see more examples and get more details from the docs
Related
I'm trying to delve deeper than the basics into React and am rebuilding a Tree library I had written in plain JS years back. I need to expose an API to users so they can programmatically add/remove nodes, select nodes, etc.
From what I've learned, a ref and context is a good approach. I've built a basic demo following the examples (without ref, for now) but I'm seeing every single tree node re-render when a selection is made, even though no props have changed for all but one.
I've tried a few things like memoizing my tree node component, etc but I feel like I'm failing to understand what's causing the re-render.
I'm using the react dev tools to highlight renders.
Here's a codesandbox demo.
My basic tree node component. I essentially map this for every node I need to show. On click, this calls select() from my context API. The rerender doesn't happen if that select() call is disabled.
const TreeNodeComponent = ({ id, text, children }) => {
console.log(`rendering ${id}`);
const { select } = useTreeContext();
const onClick = useCallback(
(event) => {
event.preventDefault();
event.stopPropagation();
select([id]);
},
[select, id]
);
return (
<div className="tree-node" onClick={onClick}>
{text}
{children ? <TreeNodesComponent nodes={children} /> : ""}
</div>
);
};
The important part of my context is the provider:
const TreeContextProvider = ({ children, nodes = [], selectedNodes }) => {
const [allNodes] = useState(nodes);
const [allSelectedNodes, setSelectedNodes] = useState(selectedNodes || []);
const api = useMemo(
() => ({
selected: () => allSelectedNodes,
select: (nodes) => {
setSelectedNodes(Array.from(new Set(allSelectedNodes.concat(nodes))));
}
}),
[allSelectedNodes]
);
const value = useMemo(
() => ({
nodes: allNodes,
selectedNodes: allSelectedNodes,
...api
}),
[allNodes, allSelectedNodes, api]
);
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
};
Again, when the call to setSelectedNodes is disabled, the rerenders don't happen. So the entire state update is triggering the render, yet individual props to all but one tree component do not change.
Is there something I can to improve this? Imagine I have 1000 nodes, I can't rerender all of them just to mark one as selected etc.
I am constructing some node objects in a function(prepareNodes) to pass to React Flow within a functional component A (lets say), and I have defined a custom node component(CardNode) stateless, which has a button. On button click it should trigger the function(prepareNodes) defined within Component A.
function ComponentA = ({ selectedNodes }) => {
const reactFlowWrapper = useRef(null);
const [elements, setElements] = useState([]);
const [edges, setEdges] = useState([]);
const prepareNode = async (nodeid) => {
//some service calls to fetch data and constuct nodes
setElements([ ...nodes]);
setEdges([...edges]);
}
return (
<ReactFlowProvider>
<div className="reactflow-wrapper" ref={reactFlowWrapper}>
<ReactFlow
nodes={elements}
edges={edges}
//some properties
>
</ReactFlow>
</div>
</ReactFlowProvider>
)
};
export default ComponentA;
function CardNode({ data }) {
const renderSubFlowNodes = (id) => {
console.log(id);
//prepareNode(id)
}
return (
<>
<Handle type="target" position={Position.Top} />
<div className="flex node-wrapper">
<button className="btn-transparent btn-toggle-node" href="#" onClick={() => renderSubFlowNodes(data['id']) }>
<div>
<img src={Icon}/>
</div>
</button>
</div>
<Handle type="source" position={Position.Bottom}/>
</>
);
}
export default CardNode;
I looked for some references online, and most of them suggest to move this resuable function out of the component, but since this function carries a state that it directly sets to the ReactFlow using useState hook, I dont think it would be much of a help.
Other references talks about using useCallback or useRefs and forwardRef, useImperativeHandle especially for functional component, Which I did not quite understand well.
Can someone suggest me a solution or a work around for this specific use-case of mine.
You can add an onClick handler to the each node, and within the node view you call this handler on click.
In the parent Component within the onClick handler you can call prepareNode as needed.
useEffect(() => {
setElements(
elements.map(item => {
...item,
onClick: (i) => {
console.log(i);
prepareNode();
},
})
)},
[]);
The classical approach is to have a parent object that defines prepareNode (along with the state items it uses) and pass the required pieces as props into the components that use them.
That "parent object" could be a common-ancestor component, or a Context (if the chain from the parent to the children makes it cumbersome to pass the props all the way down it).
Let's say I have a parent component in React with 3 separate child components of the same component class (meaning, within class Parent I have 3 Child components). How do I access each child's state within the Parent component?
My initial thoughts are to have a separate variable for each Child's state that I want to access (I only want to access the filled variable for each child). But I feel that this is certainly a sloppy solution to something that is already in place with React, so would appreciate any pointers.
Example code below for illustration purposes.
const Parent = (props) => {
return (
<div>
<Child/>
<Child/>
<Child/>
</div>
);
}
const Child = (props) => {
const [filled, setFilled] = useState(false);
return (
<div></div>
);
}
Perhaps the better question is how do I access the particular child? And once accessed, how do I access its filled state (callback function)? I've read about useRef, is that where I should be looking here?
If what you are trying to do is report back to the parent the childs's state, you can do that by passing down from the parent, via props, a function to report that state back, as such:
const Parent = (props) => {
const reportChildState = (value) =>{
//do something with the child filled state
}
return (
<div>
<Child reportState={reportChildState}/>
<Child reportState={reportChildState}/>
<Child reportState={reportChildState}/>
</div>
);
}
const Child = (props) => {
//in here you can call props.reportChildState(filled)
const [filled, setFilled] = useState(false);
return (
<div></div>
);
}
As far as I'm aware there isnt a way to access a child components state from the parent. The only solutions are to either pass the state object into the child as a prop, using the context API or using a thirst party state management such as redux.
I wouldnt use useref for accessing a child's state.
You actually wouldn't directly access the children to retrieve their states. What you want here is a Context that encompases the parent component. Check out the React Context API. In short, you can create a "context" that contains states that need to be shared between multiple components. Once the context is made, a Provider is also created with that context. This provider is a component that accepts a value prop. This prop contains an object of all the state values and setter functions within the context. Child components of a Provider component can use the useContext hook to retrieve the values and functions from the value prop of the Provider.
Code example:
MyContext.js
import React, {createContext, useState} from 'react';
// Create the context and give it default values
export const MyContext = createContext(defaultValuesObject);
// We create a component that wraps around the provider, and is stateful. The states and their setters are placed into the provider, which is then returned.
const MyProvider = (props) => {
const [firstName, setFirstName] = useState("")
const [lastName, setLastName] = useState("")
const dataToShare = {
firstName,
setFirstName,
lastName,
setLastName,
}
// Return the context provider with the data already inside, and fill the children.
return (
<MyContext.Provider value={dataToShare}>
{props.children}
</MyContext.Provider>
)
}
We have the provider now. In your parent component's code, use the useContext hook to give the component access to the values and functions stored inside the provider component.
MyParentComponent.jsx
import React, {useContext} from 'react';
import MyProvider, { MyContext } from 'MyContext'
const MyComponent = (props) => {
// useContext returns the value stored in the provider so we can use it and the functions inside. The context is maintained inside the states in the provider.
const providerValue = useContext(MyContext)
// OR
const { firstName, setFirstName, lastName, setLastName } = useContext(MyContext)
return (
<div>
// Provide the first child component with the values and functions from the context
<MyChildComponentOne someProp={providerValue} />
// Provide the second child component with the values and functions from the context
<MyChildComponentTwo someProp={providerValue} />
// Provide the second third component with the values and functions from the context
<MyChildComponentThree someProp={providerValue} />
<div/>
)
}
Now, we still won't be able to use the context without a provider, which MUST wrap around the component(s) calling useContext for that specific context. Assuming the ParentComponent is used inside of App.jsx:
App.jsx
...imports and whatever other code you have in here
/// The jsx for the App component or whatever component calls MyParentComponent
return <div>
<MyProvider>
<MyParentComponent>
</MyProvider>
</div>
To re-iterate, you are taking the state OUT of the child/parent components, and putting them INTO the Provider created by the Context. Child components of the Provider can call useContext and gain access to the data and functions in the Provider's value prop.
There is no direct way to pass information from the child to the parent, only the other way around.
But that means you can also pass functions from the parent to the child! One common pattern for achieving what you need would be to pass the state set function to the child, so it can alter the parent's state. Like so:
const Parent = (props) => {
const [childStates, setChildStates] = useState({ child1: '', child2: '', child3: '' })
return (
<div>
<Child
state={childStates.child1}
setState={(val) => setChildStates((prev) => ({ ...prev, child1: val }))}
/>
<Child
state={childStates.child2}
setState={(val) => setChildStates((prev) => ({ ...prev, child2: val }))}
/>
<Child
state={childStates.child3}
setState={(val) => setChildStates((prev) => ({...prev, child3: val }))}
/>
</div>
);
}
EDIT: This problem was caused because I was attempting to use forwardRef in conjunction with Redux connect. Take a look at this post for the solution.
I am attempting to access a DOM element in a parent component by using React.forwardRef. The problem is that ref.current never gets defined, even after the first render.
In this parent component, I want to access a DOM element from one of the child components:
const MyFunctionComponent = () => {
const bannerRef = useRef<HTMLDivElement>(null);
let bannerIndent = useRef<string>();
useEffect(() => {
// bannerRef.current is ALWAYS null. Why?
if (bannerRef.current) {
// this conditional block never runs, so bannerIndent is never defined
const bannerWidth = bannerRef.current.getBoundingClientRect().width;
bannerIndent.current = `${bannerWidth}px`;
}
}, []);
return (
<>
<Banner ref={bannerRef} />
<ComponentTwo bannerIndent={bannerIndent.current} />
</>
)
}
The component which receives the forwarded ref:
const Banner = React.forwardRef<HTMLDivElement, Props> ((props, ref) => (
<div ref={ref} />
));
The component which needs some data derived from the forwarded ref:
const ComponentTwo = (props: {bannerIndent?: string}) => (
<StyledDiv bannerIndent={props.bannerIndent} />
)
// styled.div is a styled-component
const StyledDiv = styled.div<{bannerIndent?: string}>`
... // styles, some of which depend on bannerIndent
`
Answers to existing SO questions which discuss forwardRef always being null or undefined point out that the ref will only be defined after the first render. I don't think that's the issue here, since the useEffect should run after the first render, yet bannerRef.current is still null.
The logic for setting the value of bannerIndent with a useEffect hook works correctly if used inside of the Banner component with a normal ref. However, I need to put the ref in the parent component so that bannerWidth (generated using the forwarded ref) can be passed to a sibling of the Banner component.
Help would be greatly appreciated :D
I need to pass props to selectors so that i can fetch the content of the clicked item from the selectors. However i could not pass the props. I tried this way but no success
const mapStateToProps = createStructuredSelector({
features: selectFeatures(),
getFeatureToEditById: selectFeatureToEditById(),
});
handleFeatureEdit = (event, feature) => {
event.preventDefault();
console.log("feature handle", feature);
const dialog = (
<FeatureEditDialog
feature={feature}
featureToEdit={selectFeatureToEditById(feature)}
onClose={() => this.props.hideDialog(null)}
/>
);
this.props.showDialog(dialog);
};
selectors.js
const selectFeatureState = state => state.get("featureReducer");
const selectFeatureById = (_, props) => {
console.log("props", _, props); #if i get the id of feature here
# i could then filter based on that id from below selector and show
# the result in FeatureEditDialog component
};
const selectFeatureToEditById = () =>
createSelector(
selectFeatureState,
selectFeatureById,
(features, featureId) => {
console.log("features", features, featureId);
}
);
Here is the gist for full code
https://gist.github.com/MilanRgm/80fe18e3f25993a27dfd0bbd0ede3c20
Simply pass both state and props from your mapStateToProps to your selectors.
If you use a selector directly as the mapStateToProps function, it will receive the same arguments mapState does: state and ownProps (props set on the connected component).
A simple example:
// simple selector
export const getSomethingFromState = (state, { id }) => state.stuff[id]
// reselect selector
export const getStuff = createSelector(
getSomethingFromState,
(stuff) => stuff
)
// use it as mapDispatchToProps
const mapDispatchToProps = getSomethingFromState
const MyContainer = connect(mapDispatchToProps)(MyComponent)
// and set id as an own prop in the container when rendering
<MyContainer id='foo' />
However you're doing some weird things like mapping a selector to reuse it later. It doesn't work that way, at least it's not intended to be used that way.
You use selectors to retrieve slices of your state and pass it as props to your connected components. Whenever the state changes, your selectors will be re-run (with some caching thanks to reselect). If something the component is actually retrieving from Redux has changed indeed, it will re-render.
So your FeatureEditDialog component should be connected as well, and should be capable of retrieving anything it needs from the Redux state, just by using props (which feature, which id, so on) in its own connect call.
this.props.showDialog(dialog); is a big code smell as well. ;)