Accessing a component state from a sibling button - reactjs

I'm building a page that will render a dynamic number of expandable rows based on data from a query.
Each expandable row contains a grid as well as a button which should add a new row to said grid.
The button needs to access and update the state of the grid.
My problem is that I don't see any way to do this from the onClick handler of a button.
Additionally, you'll see the ExpandableRow component is cloning the children (button and grid) defined in SomePage, which further complicates my issue.
Can anyone suggest a workaround that might help me accomplish my goal?
const SomePage = (props) => {
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { /* Need to access MyGrid state */ }} />
Add Row
</button>
<MyGrid>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
const ExpandableRowsComponent = (props) => {
const data = [{ id: 1 }, { id: 2 }, { id: 3 }];
return (
<>
{data.map((dataItem) => (
<ExpandableRow id={dataItem.id} />
))}
</>
);
};
const ExpandableRow = (props) => {
const [expanded, setExpanded] = useState(false);
return (
<div className="row-item">
<div className="row-item-header">
<img
className="collapse-icon"
onClick={() => setExpanded(!expanded)}
/>
</div>
{expanded && (
<div className="row-item-content">
{React.Children.map(props.children, (child => cloneElement(child, { id: props.id })))}
</div>
)}
</div>
);
};

There are two main ways to achieve this
Hoist the state to common ancestors
Using ref (sibling communication based on this tweet)
const SomePage = (props) => {
const ref = useRef({})
return (
<>
<MyPageComponent>
<ExpandableRowsComponent>
<button onClick={(e) => { console.log(ref.current.state) }} />
Add Row
</button>
<MyGrid ref={ref}>
<GridColumn field="somefield" />
</MyGrid>
</ExpandableRowsComponent>
</MyPageComponent>
</>
);
};
Steps required for seconds step if you want to not only access state but also update state
You must define a forwardRef component
Update ref in useEffect or pass your API object via useImerativeHandle
You can also use or get inspired by react-aptor.
⭐ If you are only concerned about the UI part (the placement of button element)
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
(Mentioned point by #Sanira Nimantha)

Related

How to fix this error in react `onClick` listener to be a function, instead got a value of `string` type

I have this codes in react:
const [categoryId, setCategoryId] = useState("");
{
catName.map((singleCategory, index) => {
const { catName, _id: categoryId } = singleCategory;
return (
<>
<div
className="category-single-div flex-3 center-flex-align-display"
key={index}
>
<p className="text-general-small2 category-custom-text">{catName}</p>
<div className="category-icons-div ">
<FaEdit
className="category-icon-edit"
onClick={() => {
setEditCategory(true);
setCategoryId(categoryId);
}}
/>
<AiFillDelete className="category-icon-edit category-icon-delete" />
</div>
</div>
</>
);
});
}
I used map to get an array of objects, and I needed their individual _id when a user clicks the edit button. I also want to call another function on the same edit button via onClick. It is working but displays an error.
Warning: Expected onClick listener to be a function, instead got a
value of string type.
I need that _id so as to pass it to a state and have access to it globally within the component at the top level.
Is this workable?
Your problem comes from the FaEdit component.
<FaEdit
id={categoryId}
className="category-icon-edit"
onClick={(editCategory, id) => { // you need to have onClick as a prop defined inside the FaEdit component
setEditCategory(editCategory);
setCategoryId(id);
}}
/>
Example...
export default function FaEdit({className, onClick, categoryId}){
const handleChange() => {
onClick(true, categoryId)
}
return(
<div className={className} onClick={() => handleChange()}>Click</div>
)
}

Insert a new instance of a component onClick outside DOM hierarchy of the parent component

I would like to create a new component( <CreateLine/>) every time the user clicks as opposed to updating underlying element in the existing DOM node.
// onClick to create a separate instance of the CreateLine Component and insert it to the Create Area as a Child.
<AddCircleSharpIcon onClick={ () => {
ReactDOM.render(
<CreateLine
type={type} title={title} line={line}
color={color}
data={[...data]}
/>,
document.getElementById("CreateArea")
)
}}
// 2ND UPDATE STILL CANT GET IT TO WORK. N number of lines show but with the same options (type,title,line,color and data). How can I get around this :)
const [numberofLines, setnumberofLines] = useState([0]);
<AddCircleSharpIcon onClick={ () => {
setnumberofLines([...numberofLines, numberofLines.length])
}}
function LineComponent() {
return (
<React.Fragment>
{numberofLines.map( (si) => (
<CreateLine
type={type} title={title} line={line}
color={color}
data={[...data]}
/>,
)) }
</React.Fragment>
)
}
// Using useEffect beacuse im running nextJs SSR.
useEffect( () => {
ReactDOM.render(<LineComponent />, document.getElementById("CreateArea"))
}, [numberofLines])
You can use the following code to add new lines every time you click the button. It is a mock code so you can fill the remaining bits and pieces.
function LineComponent() {
const [numberOfLines, setNumberOfLines] = useState([]);
const addNewLine = () => {
setNumberOfLines(prevLines => ([
...prevLines,
{
...newData // add your new data here
}
]))
}
return (
<React.Fragment>
<AddCircleSharpIcon onClick={addNewLine}
{numberOfLines.map(line => (
<CreateLine key={line.id} type={line.type} title={line.title} line={line} color={line.color} data={line.data} />
))}
</React.Fragment>
)
}
ReactDOM.render(<LineComponent />, document.getElementById("CreateArea"))

Logic to keep only one section open at a time

I'm struggling to come up with the implementation.
It's like a collapsible. I have a card component that has a local useState. Only one radio button should be active, but right now all of them can be active.
I know that using a local state, each card is going to have his own state, so they all can be active at the same time.
In this case, I'll need some kind of unified state or something like that, to handle the selection. But how can I do that?
This is my card component:
import * as S from './styled'
import Radio from '#material-ui/core/Radio'
export default function PackageCard({ packageInformations }) {
const [selectedValue, setSelectedValue] = React.useState(false)
const handleChange = () => {
setSelectedValue(!selectedValue)
}
return (
<>
<S.FormSectionContainer>
<S.PackageContainer>
<S.PackageSelectionContainer>
<S.PackageNameAndPrice>
<S.PackageName>{packageInformations.packageName}</S.PackageName>
<S.PackagePrice>{packageInformations.packagePrice}</S.PackagePrice>
</S.PackageNameAndPrice>
<Radio checked={selectedValue} onChange={handleChange} value={selectedValue} name="package" />
</S.PackageSelectionContainer>
{selectedValue && (
<S.PackageInformationContainer>
<S.PackageDescription>{packageInformations.description}</S.PackageDescription>
</S.PackageInformationContainer>
)}
</S.PackageContainer>
</S.FormSectionContainer>
</>
)
}
In my page, I just:
{packages.map(packageInfo => (
<PackageCard packageInformations={packageInfo}/>
))}
And this is what I have:
What about putting the state on the level upper?
const PackageCardsList = () => {
const [checkedIndex, setCheckedIndex] = useState();
return (
<>
{packages.map((packageInfo, index) => (
<PackageCard
checked={checkedIndex === index}
packageInformations={packageInfo}
onCheck={() => setCheckedIndex(index)}
/>
))}
<>
);
};
Then handle these two new props in the component PackageCard.
And it will be better if your cards have an ID so you will be able to store the ones instead of indexes. And don't forget about key props for rendering arrays components.

React wrapper component (HOC) does not re-render child component when it's props are changing

My wrapper component has this signature
const withReplacement = <P extends object>(Component: React.ComponentType<P>) =>
(props: P & WithReplacementProps) => {...}
Btw, full example is here https://codepen.io/xitroff/pen/BaKQNed
It's getting original content from argument component's props
interface WithReplacementProps {
getContent(): string;
}
and then call setContent function on button click.
const { getContent, ...rest } = props;
const [ content, setContent ] = useState(getContent());
I expect that content will be replaced everywhere (1st and 2nd section below).
Here's the part of render function
return (
<>
<div>
<h4>content from child</h4>
<Component
content={content}
ReplaceButton={ReplaceButton}
{...rest as P}
/>
<hr/>
</div>
<div>
<h4>content from wrapper</h4>
<Hello
content={content}
ReplaceButton={ReplaceButton}
/>
<hr/>
</div>
</>
);
Hello component is straightforward
<div>
<p>{content}</p>
<div>
{ReplaceButton}
</div>
</div>
and that's how wrapped is being made
const HelloWithReplacement = withReplacement(Hello);
But the problem is that content is being replaced only in 2nd part. 1st remains untouched.
In the main App component I also replace the content after 20 sec from loading.
const [ content, setContent ] = useState( 'original content');
useEffect(() => {
setTimeout(() => {
setContent('...too late! replaced from main component');
}, 10000);
}, []);
...when I call my wrapped component like this
return (
<div className="App">
<HelloWithReplacement
content={content}
getContent={() => content}
/>
</div>
);
And it also has the issue - 1st part is updating, 2nd part does not.
It looks like you are overriding the withReplacement internal state with the external state of the App
<HelloWithReplacement
content={content} // Remove this to stop overriding it
getContent={() => content}
/>
Anyway it looks weird to use two different states, it is better to manage your app state in only one place

How to pass state from parent to child in react?

How do I pass a state attribute from parent to child? In the following implementation, the Dropdown component has a state "isActive" and I want to access it in the Button component to attach propper styling to it. The Dropdown has to generic as it is supposed to take different sorts of buttons.
<Dropdown items="...">
<Button active ="false" />
</Dropdown>
Dropdwon.js
...
constructor(props){
super(props)
this.state = {
isActive: true,
}
}
render (){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children} /* want to set active prop for the child button here */
</div>
);
}
...
You have two possibilities:
Lift your Dropdown state and keep it in its parent component;
Use useContext hook;
The first approach would be better, but it may not be good for your application (I cannot know that). Let me make an example for both cases.
This is an example where I've lifted the isActive state to the parent component.
const ParentComponent = () => {
const [isActive, setIsActive] = useState(false);
handleIsActiveChange = (newValue) => {
setIsActive(newValue);
}
<Dropdown isActive={isActive} setIsActive={handleIsActiveChange}>
<Button isActive={isActive} />
</Dropdown>
}
const Dropdown = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
// You can use `props.handleIsActiveChange` to update the `isActive` state.
}
const Button = props => {
// You can use `props.isActive` to know whether the dropdown is active or not.
}
Instead, this exploits the useContext API:
const dropdownContext = React.createContext(null);
const Dropdown = props => {
const [isActive, setIsActive] = useState(false);
return (
<dropdownContext.Provider value={{ isActive }}>
{props.children}
</dropdownContext.Provider>
);
}
const Button = props => {
const dropdownCtx = React.useContext(dropdownContext);
// You can use `dropdownCtx.isActive` to know whether the dropdown is active or not.
}
Aside from the answer I linked, there might be another way of achieving this that I didn't see mentioned.
You can send a function as a children element of your dropdown which will take isActive as a variable :
<Dropdown items="...">
{isActive => <Button active={isActive} />}
</Dropdown>
Then, is the render function, simply call the function and send your state value as a parameter :
render(){
return (
<div className={styles.toggle} onClick={(event) => this.showMenu(event)}>
{this.props.children(this.state.isActive)}
</div>
);
}
<Dropdown >
<Button isActive={this.state.isActive} />
</Dropdown>
In your button get it with this.props.isActive

Resources