I am making a simple accordion which has text editor inside it.
If we click expand text then the text editor gets opened and if we enter some text inside the editor and click shrink, then the accordion gets closed.
Again if click on the expand text of accordion where we made the changes, then the text already entered is missing inside it.
I can understand that this re render every time we click on the expand text. Also this code,
<Text> {toggleValue === index && item.content && <EditorContainer />} </Text>
check for the item clicked then it gets opened so re render happens here and hence I am losing the entered text.
Complete working example:
https://codesandbox.io/s/react-accordion-forked-dcqbo
Could you please kindly help me to retain the value entered inside the text editor despite of the clicks over the text Expand/Shrink?
Put the editor's state into a persistent parent component. Since the NormalAccordion encompasses all editors, and you want persistent state just one editor, use another component, so that the state doesn't get lost when the editor unmounts, then pass it down for the editor to use:
const OuterEditorContainer = ({ toggleValue, setToggleValue, item, index }) => {
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const toggleHandler = (index) => {
index === toggleValue ? setToggleValue(-1) : setToggleValue(index);
};
return (
<Accordion>
<Heading>
<div
style={{ padding: "10px", cursor: "pointer" }}
className="heading"
onClick={() => toggleHandler(index)}
>
{toggleValue !== index ? `Expand` : `Shrink`}
</div>
</Heading>
<Text>
{toggleValue === index && item.content && (
<EditorContainer {...{ editorState, setEditorState }} />
)}
</Text>
</Accordion>
);
};
const NormalAccordion = () => {
const [toggleValue, setToggleValue] = useState(-1);
return (
<div className="wrapper">
{accordionData.map((item, index) => (
<OuterEditorContainer
{...{ toggleValue, setToggleValue, item, index }}
/>
))}
</div>
);
};
// text_editor.js
export default ({ editorState, setEditorState }) => (
<div className="editor">
<Editor
editorState={editorState}
onEditorStateChange={setEditorState}
toolbar={{
inline: { inDropdown: true },
list: { inDropdown: true },
textAlign: { inDropdown: true },
link: { inDropdown: true },
history: { inDropdown: true }
}}
/>
</div>
);
You could also put the state into the text_editor itself, and always render that container, but only conditionally render the <Editor.
You need to save the entered text and pass it as props from the parent component to EditorContainer.
Right now everytime you render it (e.g. when we click expand)
It looks like you set an empty state.
Something like:
EditorContainer
editorState: this.props.editorState || EditorState.createEmpty()
onEditorStateChange = (editorState) => {
// console.log(editorState)
this.props.setEditorState(editorState);
};
And in Accordion:
{toggleValue === index &&
item.content &&
<EditorContainer
editorState={this.state.editorState[index]}
setEditorState={newText => this.setState({...this.state, newText}) />}
Didn't try to execute it, but I think that's the way to achieve it.
Ps: Class components are almost not used anymore. Try to use function components and learn about useState hook, looks so much cleaner in my opinion
Related
im doing this simple ecommerce site, and on the product page you have different attributes you can choose, like sizes, colors - represented by clickable divs, with data fetched from GraphQl and then generated to the DOM through map function.
return (
<div className="attribute-container">
<div className="attribute">{attribute.id.toUpperCase()}:</div>
<div className="attribute-buttons">
{attribute.items.map((item) => {
if (type === "Color") {
return (
<AttributeButton
key={item.id}
className="color-button"
style={{ backgroundColor: item.value }}
onClick={() => addAtribute({type: type, value: item.value })}/>
);
}
return (
<AttributeButton
key={item.id}
className="size-button"
size={item.value}
onClick={() => addAtribute({ type: type, value: item.value })}
/>
);
})}
</div>
</div>
);
Im importing external component, then I check if an attribute's type is Color (type color has different styling), then render depending on that type.
What I want to implement is that when i click on one attribute button, its style changes, BUT of course when i click on another one, choose different size or color for the item i want to buy, new button's style changes AND previously selected button goes back to it's default style. Doing the first step where buttons style changes onClick is simple, but i cant wrap my head around switching between them when choosing different attributes, so only one button at the time can appear clicked.
Here is code for AttributeButton:
class Button extends PureComponent {
constructor(props){
super(props);
this.state = {
selected: false,
}
}
render() {
return (
<div
className={ !this.state.selected ? this.props.className : "size-selected "+this.props.className}
style={this.props.style}
onClick={() => {this.props.onClick(); this.setState({selected: !this.state.selected}) }}
>
{this.props.size}
</div>
);
}
}
export default Button;
PS - i have to use class components for this one, it was not my choice.
You need to have the state selected outside of your <Button> component and use it as a prop instead. Something like:
handleSelect = (button) => {
const isSelected = this.state.selected === button;
this.setState({ selected: isSelected ? null : button });
};
render() {
return (
<>
<Button
isSelected={this.state.selected === "ColorButton"}
onClick={() => this.handleSelect("ColorButton")}
/>
<Button
isSelected={this.state.selected === "SizeButton"}
onClick={() => this.handleSelect("SizeButton")}
/>
</>
);
}
My page is divided into right and left parts. On the left side there are 3 buttons, and on the right side there is a list with 3 texts. They are in different files. I want the corresponding text to come first in the list when the button is clicked. The functionality is similar to tabs, but I don’t know how to implement it, because the components are in different files and are not connected. How can I do that?
//Buttons.js
const btnArr = [
["Togle Text 1"],
["Togle Text 2"],
["Togle Text 3"],
];
const Buttons = () => {
return (
<div style={{ width: "50%" }}>
{btn.map((btn, index) => (
<Button
key={index}
text={btn}
/>
))}
);
};
//Text.js
const btnArr = [
["Text 1"],
["Text 2"],
["Text 3"],
];
const Texts = () => {
return (
<div style={{ width: "50%" }}>
{texts.map((txt, index) => (
<Text
key={index}
text={txt}
/>
))}
);
};
Parent Component
You'll want to use a useState in a parent component of both Texts and Buttons. That way you can keep track of which button has been clicked, and you'll pass Buttons a way to update which has been clicked. You'll also be able to pass Texts the value of which text is currently selected.
That parent component could look like this:
const [selectedText, setSelectedText] = useState(0);
return (
<div
>
<Buttons onSelect={setSelectedText} />
<Texts selectedText={selectedText} />
</div>
);
Buttons Component
Next we'll handle the Buttons Component. You can see in the above codeblock we are passing Buttons a prop called onSelect which we'll use to update the selectedText state.
Here's what that component could look like:
export const Buttons = ({ onSelect }) => {
return (
<div>
{btnArr.map((btn, index) => (
<Button key={index} text={btn} onClick={() => onSelect(index)} />
))}
</div>
);
};
Now, whenever a button is clicked, the selectedText state variable in the Parent will be updated to the index of the button clicked.
Texts Component
The Texts Component is a little bit trickier because we need to show the selected Text before the other Texts.
Since we are passing in selectedText as a prop, we can use that as we are creating the list. Our Texts component should look like this:
export const Texts = ({ selectedText }) => {
The most basic way to order the list is by placing our selectedText item first, followed by the mapped over text elements, but with the selectedText item filtered out. It may make more sense to look at the code:
{<Text text={texts[selectedText]} />}
{texts
.filter((txt, index) => index !== selectedText)
.map((txt, index) => (
<Text key={index} text={txt} />
))}
That way will work just fine, but if you don't want to have a <Text ... /> in two places, we can avoid that by using the following code instead. The more complicated way to do this is by using sort: we can sort through the text array to order them and then map over them like this:
{texts
.sort((txt, txt2) =>
txt === texts[selectedText]
? -1
: txt2 === texts[selectedText]
? 1
: 0
)
.map((txt, index) => (
<Text key={index} text={txt} />
))}
I've put together a full example of this on CodeSandbox here:
Extending the List
The advantage of doing it this way is that you can easily add more items to the list of buttons/text. If we simply add a ["Toggle Text 4"] to the button list and a ["Text 4"] to the text list, you can see that everything still just works.
The CodePen example demonstrates this.
Working with Different Files
In my explanation, we worked with three separate files for our code: a parent file, Texts.js, and Buttons.js.
Here's how you can use the Texts and Buttons component from inside the parent:
In the parent file at the top, import the other two files like this:
import { Texts } from "./Texts";
import { Buttons } from "./Buttons";
Then inside Texts.js, make sure to have the word export before the component is defined like this:
export const Texts = ({ selectedText }) => {
Do the same in Buttons.js:
export const Buttons = ({ onSelect }) => {
This allows us to use code from one file in a separate file. This guide gives a bit more explanation on how that works.
You can figure out that by Lifting State Up. You can try this but this isn't the best practice you can try make to order with id.
Buttons.js
const btnArr = [["Togle Text 1"], ["Togle Text 2"], ["Togle Text 3"]];
const Buttons = (props) => {
return (
<div style={{ width: "50%" }}>
{btnArr.map((btn, index) => (
<button key={index} onClick={() => props.onButtonClick(index)}>
{btn}
</button>
))}
</div>
);
};
export default Buttons;
Texts.js
const textArr = [["Text 1"], ["Text 2"], ["Text 3"]];
const Texts = (props) => {
return (
<div style={{ width: "50%" }}>
{<p>{textArr[props.order]}</p>}
{textArr.map((txt, index) => {
return index != props.order ? <p key={index}>{txt}</p> : null;
})}
</div>
);
};
export default Texts;
App.js
import { useState } from "react";
import Buttons from "./Buttons";
import Texts from "./Text";
function App() {
const [textIndex, setTextIndex] = useState(0);
function onButtonClick(buttonIndex) {
console.log(buttonIndex);
setTextIndex(buttonIndex);
}
return (
<>
<Buttons onButtonClick={onButtonClick} />
<Texts order={textIndex} />
</>
);
}
export default App;
Notice: I change the <Button> Component and the <Text> Component to facilitate the example
My advice would be to look into React-Redux. This is a state-management system that exists "outside" your component structure in a store. This allows non-related components to speak to each other.
Another option, though less clean would be to send the information from one component to the first parent that contains both components through callbacks, then pass the information through props to the other child component.
EDIT: Redux may be too complex, and too much effort depending on the complexity of the project. Passing through callbacks and props should be enough.
I am storing my data in JS array. After that I am using map function to display components with specific data. Until then, everything works.
After I click component I want to display a PopUp component that display more specific data from the data.js file. How can I pass it as props and match correct data to correct component? At this moment after I click it always shows me the same text.
My data.js file:
export const data = {
projects: [
{
name: "Project1",
image: require("../assets/img/Project1.PNG"),
description: "Set number 1",
technologies: "REACT",
link: "github.com",
},
{
name: "Project2",
image: require("../assets/img/Project2.PNG"),
description: "Project number 2 - description",
technologies: "C#",
link: "github.com",
},
This is my PopUp.js that should display more specific data:
function Popup({ project, description }) {
return (
<Wrapper>
<p>Project name is: {project}</p>
<p>Description is: {description}</p>
</Wrapper>
);
}
And here I map my data and here is the problem.
How can I pass it?
function Projects() {
const [isOpen, setIsOpen] = useState(false);
const togglePopup = () => {
setIsOpen(!isOpen);
};
return (
<Wrapper>
{data.projects.map((proj) => (
<ProjectContainer background={proj.image} onClick={togglePopup}>
{isOpen && (
<Popup project={proj.name} description={proj.description} />
)}
</ProjectContainer>
))}
</Wrapper>
);
}
Really thanks for all of the answers!
You can use index instead of boolean :
function Projects() {
const [isOpen, setIsOpen] = useState('nothing');
return (
<Wrapper>
{data.projects.map((proj, index) => (
<ProjectContainer background={proj.image}
onClick={()=>setIsOpen(oldIndex => index === isOpen ? 'nothing' : index)}>
{isOpen === index && (
<Popup project={proj.name} description={proj.description} />
)}
</ProjectContainer>
))}
</Wrapper>
);
}
The state stores the index of the current opened popup.
ProjectContainer onClick event check if the current index is the same as their and changes accordingly : to 'nothing' if they are currently opened, to their index if they are not.
The condition {isOpen === index && ( is used to show only the current opened popup ie the current index.
This way you can toggle an individual project and not all of them.
I am using Material-UI within my ReactJS app to create a table that, when clicked, expands to show more detailed info (a new row just beneath the clicked row). As example, here is a minimal toy example:
https://codesandbox.io/s/material-collapse-table-forked-t6thz
The code relevant to the problem is:
<Collapse
in={open}
timeout="auto"
TransitionProps={{
mountOnEnter: true,
unmountOnExit: true,
}}
mountOnEnter
unmountOnExit
>
<div>
{/* actual function calls here; produces JSX output */}
{console.log("This should not execute before expanding!")}
Hello
</div>
</Collapse>;
Do note that the console.log() statement is just a simple replacement for my actualy functionality, which involves some API calls that are made when a row is clicked, and the corresponding info is displayed. So instead of console.log() I would actually call some other function.
I find that the console.log() statement executed on initial page render itself, even though in=false initially. How can I prevent this? Such that the function calls take place only when the Collapse is expanded. I initially thought this would be automatically handled by using mountOnEnter and unmountOnExit, but that does not seem to be the case. Any help would be appreciated, that could fix this problem in the sample example above.
I am working on an existing open source project, and therefore do not have the flexibility to restructure the existing codebase a lot. I would ideally have loved to implement this differently, but don't have that option. So posting here to know what options I might have given the above scenario. Thanks.
Problem
The children are rendered on initial load because they're defined within the Row component.
Solution
Move the Collapse children to its own React component. This won't render the children until the Collapse is opened. However, it'll re-render the child component when Collapse is closed. So depending on how you're making the API call and how other state interacts with this component, you may want to pass open to this component and use it as an useEffect dependency.
For example:
const Example = ({ open }) => {
React.useEffect(() => {
const fetchData = async () => {...};
if(open) fetchData();
}, [open]);
return (...);
}
Demo
Code
A separate React component:
const Example = ({ todoId }) => {
const [state, setState] = React.useState({
error: "",
data: {},
isLoading: true
});
const { data, error, isLoading } = state;
React.useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId}`
);
if (res.status !== 200) throw String("Unable to locate todo item");
const data = await res.json();
setState({ error: "", data, isLoading: false });
} catch (error) {
setState({ error: error.toString(), data: {}, isLoading: false });
}
};
fetchData();
/* eslint-disable react-hooks/exhaustive-deps */
}, []);
return (
<div
style={{
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{error ? (
<p style={{ color: "red" }}>{error}</p>
) : isLoading ? (
<p>Loading...</p>
) : (
<>
<div>
<strong>Id</strong>: {data.id}
</div>
<div>
<strong>Title</strong>: {data.title}
</div>
<div>
<strong>Completed</strong>: {data.completed.toString()}
</div>
</>
)}
</div>
);
};
The Example component being used as children to Collapse (also see supported Collapse props):
<Collapse
in={open}
timeout="auto"
// mountOnEnter <=== not a supported prop
// unmountOnExit <=== not a supported prop
>
<Example todoId={todoId + 1} />
</Collapse>
Other Thoughts
If the API data is static and/or doesn't change too often, I'd recommend using data as a dependency to useEffect (similar to the open example above). This will limit the need to constantly query the API for the same data every time the same row is expanded/collapsed.
Firstly, huge thanks to Matt for his detailed explanation. I worked through his example, and expanded on it to work for me as required. The main takeaway for me was: "Move the Collapse children to its own React component."
The solution posted by Matt above, I felt, didn't completely solve the problem for me. E.g. if I add a console.log() statement to the render() of the new child component (<Example>), I still see it being executed before it is mounted.
Adding mountOnEnter and unmountOnExit solved this problem:
But as Matt mentioned, the number of times the children were getting rendered was still a problem. So I slightly changed some bits (also simplified the code a bit):
Essentially, I do this now:
My child component is:
function Example(props) {
return (
<div
style={{
fontSize: 100,
textAlign: "center",
color: "white",
backgroundColor: "#43A047"
}}
>
{props.flag && console.log("This should not execute before expanding!")}
{props.value}
</div>
);
}
and I call it from the parent component as:
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" mountOnEnter unmountOnExit>
<Example value={row.name} flag={open} />
</Collapse>
</TableCell>
</TableRow>
Note that the parameter flag is essential to avoid the function execution during closing of the <Collapse>.
I have a screen (parent) where a FlatList resides in, and the renderItem shows a Child element.
In this Child element, I have a Pressable, and when the user clicks on it, it shows a Checked Icon and changes its background colour.
This happens dynamically based off a state Array in the Parent where in each renderItem of the Child I pass the state Array.
And in the Child component I check if the ID of this Child element is present, if it is, a Checked Icon is shown and the background changes colour.
I know that states in React is asynchronous but I'm always having problems working through such scenarios.
I have tried checking in the Parent screen where the FlatList resides at, to instead pass a Boolean prop to the Child on whether to show the Checked Icon.
E.g. (Sorry always having trouble formatting code in SO)
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
isSelected={selectedIds?.includes(item)}
// selectedIds={selectedIds}
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
instead of
// In Parent Screen
<FlatList
data={displayData}
renderItem={({item}) => (
<Child
key={item}
userData={item}
id={item}
selectedIds={selectedIds} // here
selectedHandler={id => selectedHandler(id)}
/>
)}
keyExtractor={item => item}
/>
// In Child element
const Child = ({
id,
selectedIds,
selectedHandler
}) => {
return (
<Pressable
style={[
styles.checkContainer,
selectedIds?.includes(id) && { backgroundColor: '#3D9A12' }
]}
onPress={onPressHandler}
>
{selectedIds?.includes(id) && <CheckIcon />} {/* Problem lies here. Not showing Checked Icon */}
</Pressable>
);
};
I won't dump any code here as I have made a snack of the reproduction of my problem.
I appreciate any help please. Thank you so much
Unchecked:
Checked:
The problem is in the selectedHandler function.
You are storing the reference of your state in this variable.
let selectedArr = selectedIds;
and later directly modifying the state itself by doing so:
selectedArr.push(id);
This is why the state updation is not firing the re-render of your component.
Instead, what you need to do is:
let selectedArr = [...selectedIds];
By spreading it, you will be storing a copy of your array and not a reference to it. Now if you modify selectedArr, you won't modifying your state.
I made the changes in the snack provided by you and it now works fine.
The updated selectedHandler function:
const selectedHandler = id => {
let selectedArr = [...selectedIds];
console.log('before selectedArr', selectedArr);
if (selectedArr.includes(id)) {
selectedArr = selectedArr.filter(userId => userId !== id);
setSelectedIds(selectedArr);
console.log('after selectedArr', selectedArr);
return;
}
if (selectedArr.length > 2) {
selectedArr.shift();
}
selectedArr.push(id);
console.log('after selectedArr', selectedArr);
setSelectedIds(selectedArr);
};