I have a sample where I have a list of answers and a paragraph that has gaps.
I can drag the answers from the list to the paragraph gaps. After the answer fills the gap, the answer will be removed from the list. 3 answers and 3 gaps, the answer list should be empty when i drag all of them to the gaps.
But whenever I filter the listData, the component re-renders and the listData gets reset. The list always remained 2 items no matter how hard I tried. What was wrong here?
My code as below, I also attached the code sandbox link, please have a look
App.js
import GapDropper from "./gapDropper";
import "./styles.css";
const config = {
id: "4",
sort: 3,
type: "gapDropper",
options: [
{
id: "from:drop_gap_1",
value: "hello"
},
{
id: "from:drop_gap_2",
value: "person"
},
{
id: "from:drop_gap_3",
value: "universe"
}
],
content: `<p>This is a paragraph. It is editable. Try to change this text. <input id="drop_gap_1" type="text"/> . The girl is beautiful <input id="drop_gap_2" type="text"/> I can resist her charm. Girl, tell me how <input id="drop_gap_3" type="text"/></p>`
};
export default function App() {
return (
<div className="App">
<GapDropper data={config} />
</div>
);
}
gapDropper.js
import { useEffect, useState } from "react";
import * as _ from "lodash";
import styles from "./gapDropper.module.css";
const DATA_KEY = "answerItem";
function HtmlViewer({ rawHtml }) {
return (
<div>
<div dangerouslySetInnerHTML={{ __html: rawHtml }} />
</div>
);
}
function AnwserList({ data }) {
function onDragStart(event, data) {
event.dataTransfer.setData(DATA_KEY, JSON.stringify(data));
}
return (
<div className={styles.dragOptionsWrapper}>
{data.map((item) => {
return (
<div
key={item.id}
className={styles.dragOption}
draggable
onDragStart={(event) => onDragStart(event, item)}
>
{item.value}
</div>
);
})}
</div>
);
}
function Content({ data, onAfterGapFilled }) {
const onDragOver = (event) => {
event.preventDefault();
};
const onDrop = (event) => {
const draggedData = event.dataTransfer.getData(DATA_KEY);
const gapElement = document.getElementById(event.target.id);
const objData = JSON.parse(draggedData);
gapElement.value = objData.value;
onAfterGapFilled(objData.id);
};
function attachOnChangeEventToGapElements() {
document.querySelectorAll("[id*='drop_gap']").forEach((element) => {
element.ondragover = onDragOver;
element.ondrop = onDrop;
});
}
useEffect(() => {
attachOnChangeEventToGapElements();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
<HtmlViewer rawHtml={data} />
</div>
);
}
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
clonedGaps = clonedGaps.filter((g) => g.id !== id);
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
export default GapDropper;
Code sandbox
the problem is that you are not keeping on track which ids you already selected, so thats why the first time it goes right, and then the second one, the values just replace the last id.
Without changing a lot of your code, we can accomplish by tracking the ids inside a ref.
const GapDropper = ({ data }) => {
const [gaps, setGaps] = useState(() => {
return data.options;
});
const ids = useRef([])
function onAfterGapFilled(id) {
let clonedGaps = _.cloneDeep(gaps);
ids.current = [...ids.current, id]
clonedGaps = clonedGaps.filter((g) => !ids.current.includes(g.id));
setGaps(clonedGaps);
}
return (
<div>
<div>
<AnwserList data={gaps} />
<Content
data={data.content}
onAfterGapFilled={(e) => onAfterGapFilled(e)}
/>
</div>
</div>
);
};
Maybe there is a better solution but this one does the job
Related
import React, { useState } from "react";
import { v4 as uuidv4 } from 'uuid';
const ReturantInfo = ()=>{
const data = [
{
name: "Haaland Restaurant's",
stars: 5,
price:"100$",
ranking:"top 1 in england",
ids:[
{id:uuidv4(),key:uuidv4(),visibility:false},
{id:uuidv4(),key:uuidv4(),visibility:false},
{id:uuidv4(),key:uuidv4(),visibility:false},
]
}
]
const [restaurantData,setRestaurantData] = useState(data)
const CardElement = restaurantData.map((data)=>{
return(
<div style={{color:"white"}}>
<h1>{data.name}</h1>
<div>
<div>
<h1>Stars</h1>
<p
id={data.ids[0].id}
onClick={()=>toggleVisibility(data.ids[0].id)}
>show</p>
</div>
{ data.ids[0].visibility ? <p>{data.stars}</p> : ""}
</div>
<div>
<div>
<h1>Price</h1>
<p
id={data.ids[1].id}
onClick={()=>toggleVisibility(data.ids[1].id)}
>show</p>
</div>
{ data.ids[1].visibility ? <p>{data.price}</p> : ""}
</div>
<div>
<div>
<h1>Ranking</h1>
<p
id={data.ids[2].id}
onClick={()=>toggleVisibility(data.ids[2].id)}
>show</p>
</div>
{ data.ids[2].visibility ? <p>{data.ranking}</p> : ""}
</div>
</div>
)
})
function toggleVisibility(id) {
setRestaurantData((prevData) =>
prevData.map((data) => {
data.ids.map(h=>{
return h.id === id ? {...data,ids:[{...h,visibility:!h.visibility}]} : data
})
})
);
}
return(
<div>
{CardElement}
</div>
)
}
export default ReturantInfo
that's a small example from my project I want to toggle visibility property by depending on the id of the clicked element and then if it equals to the id in the array then change the visibility to the opposite.
It looks like your handler isn't mapping the data to the same shape as what was there previously. Your data structure is quite complex which doesn't make the job easy, but you could use an approach more like this, where you pass in the element of data that needs to be modified, as well as the index of hte ids array that you need to modify, then return a callback that you can use as the onClick handler:
function toggleVisibility(item, index) {
return () => {
setRestaurantData((prevData) => prevData.map((prevItem) => {
if (prevItem !== item) return prevItem;
return {
...prevItem,
ids: prevItem.ids.map((id, i) =>
i !== index ? id : { ...id, visibility: !id.visibility }
),
};
}));
};
}
And you'd use it like this:
<p onClick={toggleVisibility(data, 0)}>show</p>
While you're there, you could refactor out each restaurant "property" into a reusable component, since they're all effectively doing the same thing. Here's a StackBlitz showing it all working.
function toggleVisibility(id) {
setRestaurantData(
restaurantData.map((d) => {
const ids = d.ids.map((h) => {
if (h.id === id) {
h.visibility = !h.visibility;
}
return h;
});
return { ...d, ids };
})
);
}
I want to expand a demo provided by some tutorial about React Design Patterns, subject: Controlled Onboarding Flows, to implement multiple forms on several steps via Onboarding. But unfortunately the tutor did stop at the exciting part when it comes to having two-directional flows.
So I'm stuck and don't understand how to select the resp. function (marked with "// HOW TO DECIDE?!" in the 2nd code segment here).
So, every time I hit the prev. button, I receive the "Uncaught TypeError: goToPrevious is not a function" message, because both are defined.
Any suggestions on how to handle this?
This is what I got so far.
The idea behind this is to get the data from each form within the respo. Step Component and manage it witihin the parent component - which atm happens to be the App.js file.
Any help, tips, additional sources to learn this would be highly appreciated.
This is my template for the resp. controlled form components I want to use:
export const ControlledGenericForm = ({ formData, onChange }) => {
return (
<form>
{Object.keys(formData).map((formElementKey) => (
<input
key={formElementKey}
value={formData[formElementKey]}
type="text"
id={formElementKey}
onInput={(event) => onChange(event.target.id, event.target.value)}
/>
))}
</form>
);
};
That's my controlled Onboarding component, I want to use:
import React from "react";
export const ControlledOnboardingFlow = ({
children,
currentIndex,
onPrevious,
onNext,
onFinish,
}) => {
const goToNext = (stepData) => {
onNext(stepData);
};
const goToPrevious = (stepData) => {
onPrevious(stepData);
};
const goToFinish = (stepData) => {
onFinish(stepData);
};
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
// HOW TO DECIDE?!
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
if (currentChild && onPrevious)
return React.cloneElement(currentChild, { goToPrevious });
return currentChild;
};
And that's the actual use of this two components within my App:
import { useState } from "react";
import { ControlledOnboardingFlow } from "./ControlledComponents/ControlledOnboardingFlow";
import { ControlledGenericForm } from "./ControlledComponents/ControlledGenericForm";
function App() {
const [onboardingData, setOnboardingData] = useState({
name: "Juh",
age: 22,
hair: "green",
street: "Main Street",
streetNo: 42,
city: "NYC",
});
const [currentIndex, setCurrentIndex] = useState(0);
const formDataPartOne = (({ name, age, hair }) => ({ name, age, hair }))(
onboardingData
);
const formDataPartTwo = (({ street, streetNo, city }) => ({
street,
streetNo,
city,
}))(onboardingData);
const onNext = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex + 1);
};
const onPrevious = (stepData) => {
setOnboardingData({ ...onboardingData, ...stepData });
setCurrentIndex(currentIndex - 1);
};
const onFinish = () => {
console.log("Finished");
console.log(onboardingData);
};
const handleFormUpdate = (id, value) => {
setOnboardingData({ ...onboardingData, [id]: value });
};
const StepOne = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 1</h1>
<ControlledGenericForm
formData={formDataPartOne}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)} >
Prev
</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepTwo = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 2</h1>
<ControlledGenericForm
formData={formDataPartTwo}
onChange={handleFormUpdate}
/>
<button onClick={() => goToPrevious(onboardingData)}>Prev</button>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
const StepThree = ({ goToPrevious, goToNext }) => (
<>
<h1>Step 3</h1>
<h3>
Congrats {onboardingData.name} for being from, {onboardingData.city}
</h3>
<button onClick={() => goToNext(onboardingData)}>Next</button>
</>
);
return (
<ControlledOnboardingFlow
currentIndex={currentIndex}
onPrevious={onPrevious}
onNext={onNext}
onFinish={onFinish}
>
<StepOne />
<StepTwo />
{onboardingData.city === "NYC" && <StepThree />}
</ControlledOnboardingFlow>
);
}
export default App;
if (currentChild && onNext)
return React.cloneElement(currentChild, { goToNext });
Since onNext exists, this is the code that will run. It clones the element and gives it a goToNext prop, but it does not give it a goToPrevious prop. So when you press the previous button and run code like onClick={() => goToPrevious(onboardingData)}, the exception is thrown.
It looks like you want to pass both functions into the child, which can be done like:
const currentChild = React.Children.toArray(children)[currentIndex];
if (currentChild === undefined) goToFinish();
if (currentChild) {
return React.cloneElement(currentChild, { goToNext, goToPrevious });
}
return currentChild;
If one or both of them happens to be undefined, then the child will get undefined, but that's what you would do anyway with the if/else.
When selecting files and dropping them into the drag-and-drop zone
I implemented it so that the file row list is output.
There is a problem.
The check box is automatically checked.
In developer tools, the confirmChecked function is called as many as the number of file rows.
Could this be related?
Why is the onChecked function automatically executed when drag and drop?
If anyone knows the cause of this problem or how to fix it, please let me know.
import React, { useCallback, useState } from 'react';
import { useDropzone } from 'react-dropzone';
import '../styles/FileAttach2.scss';
function FileAttach2(props) {
// state
// 체크된 항목
const [indexesArrayForChekedList, setIndexesArrayForChekedList] = useState([]);
const [filesToUpload, setFilesToUpload] = useState([]);
// 1122
const moveRowUp = params => {
};
const moveRowDown = params => {
};
// checkbox 관련
const confirmChecked = useCallback(
index => {
if (indexesArrayForChekedList.includes(index)) {
return false;
} else {
return true;
}
},
[indexesArrayForChekedList],
);
const handleCheckedStatus = useCallback(
(e, idx) => {
const checked = e.target.checked;
if (checked) {
const indexArrayForUpdate = indexesArrayForChekedList.filter(el => {
return el != idx;
});
setIndexesArrayForChekedList(indexArrayForUpdate);
} else {
setIndexesArrayForChekedList(prev => [...prev, idx]);
}
},
[indexesArrayForChekedList],
);
const deleteRowByIndex = useCallback(
idx => {
const afterDeleteRow = filesToUpload.filter(file => {
return file.index != idx;
});
setFilesToUpload(afterDeleteRow);
},
[filesToUpload],
);
// dropzone 관련
const onDrop = acceptedFiles => {
const filesData = acceptedFiles.map((file, index) => {
return { index: filesToUpload.length + index, name: file.name, size: file.size };
});
setFilesToUpload(prev => [...prev, ...filesData]);
};
const { getRootProps, getInputProps, open, acceptedFiles } = useDropzone({
noClick: true,
noKeyboard: true,
onDrop,
});
// FileRow template
const files = filesToUpload.map(file => (
<div className="fileRow" key={file.index}>
<div>
<input
type="checkBox"
onChange={e => handleCheckedStatus(e, file.index)}
checked={confirmChecked(file.index)}
/>
</div>
<div>{file.name}</div>
<div>{file.size} (bytes)</div>
<button type="button" onClick={() => deleteRowByIndex(file.index)}>
삭제
</button>
</div>
));
return (
<div className="">
<button type="button" onClick={open}>
Open File Dialog
</button>
<div {...getRootProps({ className: 'dropzone' })}>
<input {...getInputProps()} />
{filesToUpload.length !== 0 ? (
<div>
<div className="fileRowHeader">
<button onClick={moveRowDown}>아래로</button>
<button onClick={moveRowUp}>위로</button>
</div>
<div>{files}</div>
</div>
) : (
''
)}
<div></div>
</div>
</div>
);
}
export default FileAttach2;
You are returning true when indexesArrayForChekedList includes your index which will always be true at the beginning.
Try below code in your confirmChecked function
const confirmChecked = useCallback(
index => indexesArrayForChekedList.includes(index),
[indexesArrayForChekedList],
);
I have a very basic application to test how to prevent unnecessary rendering, but I'm very confused as it is not working no matter what I try. Please take a look.
App.js
import { useState, useCallback } from "react";
import User from "./User";
let lastId = 0;
function App() {
const [users, setUsers] = useState([
{ id: 0, name: "Nicole Kidman", gender: "Female" },
]);
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
const newUsers = [...users];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
setUsers(newUsers);
}
},
[users]
);
const addNewUser = useCallback(() => {
let newUser = { id: ++lastId, name: "John Doe", gender: "Male" };
setUsers((prevUsers) => [...prevUsers, newUser]);
}, []);
return (
<div className="App">
<button onClick={addNewUser}>Add user</button>
<br />
{users.map((user) => (
<User key={user.id} user={user} handleUserChange={handleUserChange} />
))}
</div>
);
}
export default App;
User.js
import { useRef, memo } from "react";
const User = memo(({ user, handleUserChange }) => {
const renderNum = useRef(0);
return (
<div className="user">
<div> Rendered: {renderNum.current++} times</div>
<div>ID: {user.id}</div>
<div>
Name:{" "}
<input
name="name"
value={user.name}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<div>
Gender:{" "}
<input
name="gender"
value={user.gender}
onChange={(e) => handleUserChange(e, user.id)}
/>
</div>
<br />
</div>
);
});
export default User;
Why the useCallback and memo doesn't do the job here? How can I make it work, prevent rendering of other User components if another User component is changing(typing something in Input)?
Thank you.
useCallback and useMemo take a dependency array. If any of the values inside that array changes, React will re-create the memo-ized value/callback.
With this in mind, we see that your handleUsersChange useCallback is recreated every time the array users changes. Since you update the users state inside the callback, every time you call handleUsersChange, the callback is re-created, and therefore the child is re-rendered.
Solution:
Don't put users in the dependency array. You can instead access the users value inside the handleUsersChange callback by providing a callback to setUsers functions, like so:
const handleUserChange = useCallback(
(e, userId) => {
const { name, value } = e.target;
setUsers((oldUsers) => {
const newUsers = [...oldUsers];
const index = newUsers.findIndex((user) => user.id === userId);
if (index >= 0) {
newUsers[index] = {
...newUsers[index],
[name]: value,
};
return newUsers;
}
return oldUsers;
})
},
[]
);
I've created a simple example of how useCallback is not allowing me to preserve state changes. When I remove the useCallback, the counters that I store in state update as expected, but adding useCallback (which I was hoping would keep rerenders of all speaker items to not re-render) keeps resetting my state back to the original (0,0,0).
The problem code is here in codesandbox:
https://codesandbox.io/s/flamboyant-shaw-2wtqj?file=/pages/index.js
and here is the actual simple one file example
import React, { useState, memo, useCallback } from 'react';
const Speaker = memo(({ speaker, speakerClick }) => {
console.log(speaker.id)
return (
<div>
<span
onClick={() => {
speakerClick(speaker.id);
}}
src={`speakerimages/Speaker-${speaker.id}.jpg`}
width={100}
>{speaker.id} {speaker.name}</span>
<span className="fa fa-star "> {speaker.clickCount}</span>
</div>
);
});
function SpeakerList({ speakers, setSpeakers }) {
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={useCallback((id) => {
const speakersNew = speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
setSpeakers(speakersNew);
},[])}
key={speaker.id}
/>
);
})}
</div>
);
}
//
const App = () => {
const speakersArray = [
{ id: 1124, name: 'aaa', clickCount: 0 },
{ id: 1530, name: 'bbb', clickCount: 0 },
{ id: 10803, name: 'ccc', clickCount: 0 },
];
const [speakers, setSpeakers] = useState(speakersArray);
return (
<div>
<h1>Speaker List</h1>
<SpeakerList speakers={speakers} setSpeakers={setSpeakers}></SpeakerList>
</div>
);
};
export default App;
first, you can only use a hook at component body, you can't wrap it at speakerClick props function declaration. second, useCallback will keep the original speakers object reference, which will be a stale value. To solve this, you can use setSpeakers passing a callback instead, where your function will be called with the current speakers state:
function SpeakerList({ speakers, setSpeakers }) {
const speakerClick = useCallback(
(id) => {
// passing a callback avoid using a stale object reference
setSpeakers((speakers) => {
return speakers.map((speaker) => {
return speaker.id === id
? { ...speaker, clickCount: speaker.clickCount + 1 }
: speaker;
});
});
},
[setSpeakers] // you can add setSpeakers as dependency since it'll remain the same
);
return (
<div>
{speakers.map((speaker) => {
return (
<Speaker
speaker={speaker}
speakerClick={speakerClick}
key={speaker.id}
/>
);
})}
</div>
);
}