How to access parent props from child component - react - reactjs

Let's say that I want to build a reusable react Accordion Component, which will have an AccordionSummary and an AccordionDetails child like this:`
<Accordion>
<AccordionTitle>This is an accordion title</AccordionTitle>
<AccordionSummary>This is the accordion summary, which will be shown if the user clicks the accordion title </AccordionSummary>
</Accordion>
How can I make it, so that when the AccordionTitle is clicked, the summary will be shown to the corresponding accordion. Is there a way to share data between react child and parent components for each individual accordion in this case.

You can use a context here. Sorry for promoting, but I wrote a detailed article on this topic : https://rocambille.github.io/en/2022/05/02/how-to-do-a-modal-in-react-the-html-first-approach/
This could lead to something like that for your Accordion:
const AccordionContext = createContext();
function Accordion({ children }) {
const [someState, setSomeState] = useState();
return (
<AccordionContext.Provider value={ { someState, setSomeState } }>
{children}
</AccordionContext.Provider>
);
}
function AccordionTitle({ children }) {
const { someState, setSomeState } = useContext(AccordionContext);
return (
...
);
}
function AccordionSummary({ children }) {
const { someState, setSomeState } = useContext(AccordionContext);
return (
...
);
}
But as stated in my article, you may want to consider HTML stuff like the summary/details tags ;)

Related

Observe (get sized) control (listen to events) over a nested component in the react and typescript application via the forwardRef function

I have a functional component called MyDivBlock
const MyDivBlock: FC<BoxProps> = ({ }) => {
{getting data...}
return (
<>
<div className='divBlock'>
{data.map((todo: { id: string; title: string }) =>
<div key={todo.id}>{todo.id} {todo.title} </div>)}
</div>
</>
);
};
I use it in such a way that MyDivBlock is nested as a child of
const App: NextPage = () => {
return (
<div>
<Box >
<MyDivBlock key="key0" areaText="DIV1" another="another"/>
</Box>
</div>
)
}
Note that MyDivBlock is nested in Box and MyDivBlock has no ref attribute. This is important because I need to write Box code with no additional requirements for my nested children. And anyone who will use my Box should not think about constraints and ref attributes.
Then I need to get the dimensions of MyDivBlock in the code of Box component, and later attach some event listeners to it, such as scrolling. These dimensions and listeners will be used in the Box component. I wanted to use Ref to control it. That is, the Box will later observe changes in the dimensions and events of MyDivBlock by creating a ref-reference to them
I know that this kind of parent-child relationship architecture is implemented through forwardRef
And here is the Box code:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
export interface BoxProps extends React.ComponentProps<any> {
children?: Element[];
className: string;
}
export const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const methods = {
show() {
if (childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, function (item) {
console.log(item)})
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
return <div ref={childRef}> {children} </div>
});
ChildWithForwardRef.displayName = 'ChildWithForwardRef';
return (
<div
className={'BoxArea'}>
<button name="ChildComp" onClick={() => childRef.current.show()}>get Width</button>
<ChildWithForwardRef ref={childRef} />
</div>
);
}
export default Box;
The result of pressing the button:
childRef.current is present...
[...]
$$typeof: Symbol(react.element) key: "key0" props: {areaText: 'DIV1', another: 'another'}
[...] Object
offsetWidth = undefined
As you can see from the output, the component is visible through the created ref. I can even make several nested ones and get the same for all of them.
But the problem is that I don't have access to the offsetWidth and other properties.
The other challenge is how can I add the addEventListener?
Because it works in pure Javascript with their objects like Element, Document, Window or any other object that supports events, and I have ReactChildren objects.
Plus I'm using NextJS and TypeScript.
Didn't dive too deep into the problem, but this may be because you are passing the same childRef to both div inside ChildWithForwardRef and to ChildWithForwardRef itself. The latter overwrites the former, so you have the method .show from useImperativeHandle available but not offsetWidth. A quick fix is to rewrite ChildWithForwardRef to use its own ref:
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const ref = useRef<HTMLDivElement>()
const methods = {
show() {
if (ref.current) {
console.log("ref.current is present...");
React.Children.forEach(children, (item) => console.log(item))
console.log("offsetWidth = " + ref.current.offsetWidth);
} else {
console.log("ref.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
// Here ref instead of childRef
return <div ref={ref}> {children} </div>
});
But really I don't quite get why you would need ChildWithForwardRef at all. The code is basically equivalent to this simpler version:
const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const showWidth = () => {
if(childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, item => console.log(item))
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
}
return (
<div className={'BoxArea'}>
<button name="ChildComp" onClick={showWidth}>get Width</button>
<div ref={childRef}>{children}</div>
</div>
);
}
You can't solve this completely with React. I solved it by wrapping the child component, making it take the form of the parent.

React Context value gets updated, but component doesn't re-render

This Codesandbox only has mobile styles as of now
I currently have a list of items being rendered based on their status.
Goal: When the user clicks on a nav button inside the modal, it updates the status type in context. Another component called SuggestionList consumes the context via useContext and renders out the items that are set to the new status.
Problem: The value in context is definitely being updated, but the SuggestionList component consuming the context is not re-rendering with a new list of items based on the status from context.
This seems to be a common problem:
Does new React Context API trigger re-renders?
React Context api - Consumer Does Not re-render after context changed
Component not re rendering when value from useContext is updated
I've tried a lot of suggestions from different posts, but I just cannot figure out why my SuggestionList component is not re-rendering upon value change in context. I'm hoping someone can give me some insight.
Context.js
// CONTEXT.JS
import { useState, createContext } from 'react';
export const RenderTypeContext = createContext();
export const RenderTypeProvider = ({ children }) => {
const [type, setType] = useState('suggestion');
const renderControls = {
type,
setType,
};
console.log(type); // logs out the new value, but does not cause a re-render in the SuggestionList component
return (
<RenderTypeContext.Provider value={renderControls}>
{children}
</RenderTypeContext.Provider>
);
};
SuggestionPage.jsx
// SuggestionPage.jsx
export const SuggestionsPage = () => {
return (
<>
<Header />
<FeedbackBar />
<RenderTypeProvider>
<SuggestionList />
</RenderTypeProvider>
</>
);
};
SuggestionList.jsx
// SuggestionList.jsx
import { RenderTypeContext } from '../../../../components/MobileModal/context';
export const SuggestionList = () => {
const retrievedRequests = useContext(RequestsContext);
const renderType = useContext(RenderTypeContext);
const { type } = renderType;
const renderedRequests = retrievedRequests.filter((req) => req.status === type);
return (
<main className={styles.container}>
{!renderedRequests.length && <EmptySuggestion />}
{renderedRequests.length &&
renderedRequests.map((request) => (
<Suggestion request={request} key={request.title} />
))}
</main>
);
};
Button.jsx
// Button.jsx
import { RenderTypeContext } from './context';
export const Button = ({ handleClick, activeButton, index, title }) => {
const tabRef = useRef();
const renderType = useContext(RenderTypeContext);
const { setType } = renderType;
useEffect(() => {
if (index === 0) {
tabRef.current.focus();
}
}, [index]);
return (
<button
className={`${styles.buttons} ${
activeButton === index && styles.activeButton
}`}
onClick={() => {
setType('planned');
handleClick(index);
}}
ref={index === 0 ? tabRef : null}
tabIndex="0"
>
{title}
</button>
);
};
Thanks
After a good night's rest, I finally solved it. It's amazing what you can miss when you're tired.
I didn't realize that I was placing the same provider as a child of itself. Once I removed the child provider, which was nested within itself, and raised the "parent" provider up the tree a little bit, everything started working.
So the issue wasn't that the component consuming the context wasn't updating, it was that my placement of providers was conflicting with each other. I lost track of my component tree. Dumb mistake.
The moral of the story, being tired can make you not see solutions. Get rest.

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

React: Design pattern that uses a Ref to set style of the parent component

I would like to ask if this is a sensible component design pattern in React.
Let's say I have the following components, App, ContentContainer and WithBlueBackground. The idea is to use a Ref to set a blue background on the ContentContainer with the WithBlueBackground component.
The simplified code would look something like this.
// App.js
export function App() => {
const contentContainerRef = useRef();
return (
<ContentContainer contentContainerRef={contentContainerRef}>
<WithBlueBackground contentContainerRef={contentContainerRef}>
</WithBlueBackground>
</ContentContainer>
)
}
// ContentContainer
export function ContentContainer(props) => {
return (
<div ref={props.contentContainerRef}>
// Some content
</div>
)
}
// WithBlueBackground
export function ContentContainer(props) => {
useEffect(() => {
if (props.containerRef && props.contentContainerRef.current) {
props.contentContainerRef.current.style.backgroundColor = 'blue';
}
}, [props.contentContainerRef])
return <>{ props.children }</>;
}
This way if I want to have a green background in the content container I can create a new component that sets this style without the ContentContainer having to know about this. This increases the composability of the code which is promote in the react docs.
Nevertheless, passing the refs is a bit ugly.
My question is, is this a sensible pattern and if not is there another way to achieve what I am trying to do here.
If it is a direct child, you could just pass an update function around:
// ContentContainer
export function ContentContainer(props) {
const [backgroundColor, setColor] = React.useState("white");
return (
<div style={{ backgroundColor }}>
<ChildComponent setColor={color => setColor(color)}>// Some content</ChildComponent>;
</div>
);
}
// WithBlueBackground
export function ChildComponent(props) {
React.useEffect(() => {
props.setColor("blue");
}, []);
return <>{props.children}</>;
}
If it is deeper nested, you could use the context API. The same principle applies.enter link description here

React.useEffect stack execution prevents parent from setting defaults

I have attached a simplified example that demonstrates my issue:
https://codesandbox.io/s/reactusehook-stack-issue-piq15
I have a parent component that receives a configuration, of which screen should be rendered. the rendered screen should have control over the parent appearance. In the example above I demonstrated it with colors. But the actual use case is flow screen that has next and back buttons which can be controlled by the child.
in the example I define common props for the screens:
type GenericScreenProps = {
setColor: (color: string) => void;
};
I create the first screen, that does not care about the color (parent should default)
const ScreenA = (props: GenericScreenProps) => {
return <div>screen A</div>;
};
I create a second screen that explicitly defines a color when mounted
const ScreenB = ({ setColor }: GenericScreenProps) => {
React.useEffect(() => {
console.log("child");
setColor("green");
}, [setColor]);
return <div>screen B</div>;
};
I create a map to be able to reference the components by an index
const map: Record<string, React.JSXElementConstructor<GenericScreenProps>> = {
0: ScreenA,
1: ScreenB
};
and finally I create the parent, that has a button that swaps the component and sets the default whenever the component changes
const App = () => {
const [screenId, setScreenId] = useState(0);
const ComponentToRender = map[screenId];
const [color, setColor] = useState("red");
React.useEffect(() => {
console.log("parent");
setColor("red"); // default when not set should be red
}, [screenId]);
const onButtonClick = () => setScreenId((screenId + 1) % Object.keys(map).length)
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
<ComponentToRender setColor={setColor} />
</div>
);
};
In this example, the default color should be red, for screen A. and green for the second screen.
However, the color stays red because useEffect is using a stack to execute the code. if you run the code you will see that once the button clicked there will be child followed by parent in log.
I have considered the following solution, but they are not ideal:
each child has to explicitly define the color, no way to enforce it without custom lint rules
convert the parent into a react class component, there has to be a hooks solution
This might be an anti-pattern where child component controls how its parent behave, by I could not identify a way of doing that without replicating the parent container for each screen. the reason I want to keep a single parent is to enable transition between the screens.
If I understood the problem correctly, there is no need to pass down setColor to the children. Making expressions more explicit might make a bit longer code, but I think it helps in readability. As what you shared is a simplified example, please let me know if it fits your real case:
const ScreenA = () => {
return <div>screen A</div>;
};
const ScreenB = () => {
return <div>screen B</div>;
};
const App = () => {
const [screen, setScreen] = useState<"a" | "b">("a");
const [color, setColor] = useState<"red" | "green">("red");
const onButtonClick = () => {
if (screen === "a") {
setScreen("b");
setColor("green");
} else {
setScreen("a");
setColor("red");
}
};
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
{screen === "a" ? <ScreenA /> : <ScreenB />}
</div>
);
};
render(<App />, document.getElementById("root"));

Resources