I'm trying to make a CRUD person function where each person has an array of skills.
I want a function where you're able to add/edit/remove skills on a given person.
Each array consist of a skill element as a string and a star element as an integer. I've made some dynamic inputfields with an add and a remove function for more/less inputfields in a bootstrap modal.
The data is fetched from Firebase with a useEffect and set as setData in EditPerson.jsx. No problem here.
The issue consist of 3 components atm: EditPerson -> ModalEditSkills -> EditSkills. (Please let me know if this is a bad structure).
I'm now able to set the useState of newData in SkillEdit.jsx with the correct data. This makes sure that on EditPerson I'll be able to view the correct data input from given in the EditSkills. Also if I console.log the data in EditSkills I can see that it works like a charm. But when I close the bootstrap modal and open it again the useState in index 0 have been reset to init useState (0).
I can't add images in the text here yet, so here's some links for the images if needed.
The image explains that the console.log tells me that the useState is set correct, but it stills reset the state of index 0 everytime I re-open the modal.
Hope that makes sense otherwise let me know.
ReactStars-choosen
Console.log
EditPerson.jsx
const EditPerson = () => {
const [data, setData] = useState({});
const [skills, setSkills] = useState([]);
const { id } = useParams();
useEffect(() => {
if (id) {
const fetchData = async () => {
const docRef = doc(db, "person", id);
try {
const docSnap = await getDoc(docRef);
setData(docSnap.data());
} catch (error) {
console.log(error);
}
};
fetchData().catch(console.error);
} else {
setData("");
}
}, [id]);
useEffect(() => {
if (data) {
setSkills(data.skills);
}
}, [data]);
const handleSkills = (skill) => {
setSkills(skill);
};
return (
<div>
<ModalEditSkills
handleSkills={handleSkills}
data={skills}
/>
</div>
);
}
ModalEditSkills.jsx
const ModalEditSkills = ({ data, handleSkills }) => {
const [show, setShow] = useState(false);
const [newData, setNewData] = useState({});
useEffect(() => {
if (data) {
setNewData(data);
}
}, [data]);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const handleSubmitSkills = (e) => {
e.preventDefault();
handleSkills(newData);
setShow(false);
};
return (
<>
<div className="content_header">
<div className="content_header_top">
<div className="header_left">Skills</div>
<div className="header_right">
<Button className="round-btn" onClick={handleShow}>
<i className="fa-solid fa-pencil t-14"></i>
</Button>
</div>
</div>
</div>
<Modal show={show} onHide={handleClose} size="">
<Modal.Header closeButton>
<Modal.Title>Edit Person</Modal.Title>
</Modal.Header>
<Modal.Body>
<SkillEdit data={data} setNewData={setNewData} />
</Modal.Body>
<Modal.Footer>
<Form>
<Button className="btn-skill-complete" onClick={handleSubmitSkills}>
Save
</Button>
</Form>
</Modal.Footer>
</Modal>
</>
);
};
SkillEdit.jsx
const SkillEdit = ({ data, setNewData }) => {
const [inputField, setInputField] = useState([{ skill: "", stars: 0 }]);
const handleAddFields = () => {
setInputField([...inputField, { skill: "", stars: 0 }]);
};
const handleRemoveFields = (index) => {
const values = [...inputField];
values.splice(index, 1);
setInputField(values);
setNewData(values);
};
const handleChangeInput = (index, name, value) => {
const values = [...inputField];
values[index][name] = value;
setInputField(values);
setNewData(values);
};
useEffect(() => {
if (data) {
const setSkills = () => {
setInputField(data);
};
setSkills();
}
}, [data]);
return (
<Form>
<div>
{inputField?.map((inputField, index) => (
<div key={index}>
<Row>
<Col xs={5} md={5}>
<Form.Group as={Col}>
<Form.Control
className="mb-3"
type="text"
id="skill"
name="skill"
value={inputField?.skill}
onChange={(event) =>
handleChangeInput(index, "skill", event.target.value)
}
/>
</Form.Group>
</Col>
<Col xs={4} md={4}>
<div>
<Form.Group>
<ReactStars
type="number"
name="stars"
count={5}
size={24}
id="stars"
onChange={(newValue) =>
handleChangeInput(index, "stars", newValue)
}
emptyIcon={<i className="fa-solid fa-star"></i>}
filledIcon={<i className="fa-solid fa-star"></i>}
value={inputField.stars}
/>
</Form.Group>
</div>
</Col>
<Col xs={3} md={3}>
<div>
<button
type="button"
onClick={() => handleAddFields()}
>
<i className="fa-solid fa-plus"></i>
</button>
<button
type="button"
onClick={() => handleRemoveFields(index)}
>
<i className="fa-solid fa-minus"></i>
</button>
</div>
</Col>
</Row>
</div>
))}
</div>
</Form>
);
};
This took some time for me to work out. I had trouble reproducing and still do, but I noticed a lot of odd behaviour around the stars. In the end, I've figured out its probably this bug in the react-stars package.
Unfortunately, the value prop does not actually control the value after the initial render. So It's like an uncontrolled component. The library therefore, is poor. It hasn't been committed to for 4 years. Usually, if a component is uncontrolled, the developer calls the prop initialValue or defaultValue instead of value, which usually implies the component is controlled. Here, the author has made a mistake. Regardless, in your case, you need controlled mode.
It's possible theres another bug interacting. But I'd start by replacing react-stars as not being able to have controlled mode is extremely poor and it makes it very hard to see the wood through the trees. There is a "solution" in the github thread but its a massive hack -- its using the special key property to remount it every time the value changes.
I went looking for an alternative and much to my surprise a lot of the other libraries are also uncontrolled -- which really sucks. What you could do instead of the hack in the github issue, is make it so the dialog is unmounted when open is false. This would mean each time the dialog opens it resets the value back to that which is held in the parent state. See my bottom code for that solution.
There's good options though here and here but they are part of larger design systems, and its probably overkill to bring in a whole other design system when you have committed to bootstrap. Depending on how early you are in your project though, I'd seriously consider switching to something like MUI. Personal opinion territory, but bootstrap is pretty outdated and the React wrapper and associated community, plus diversity of components, is much smaller. It shows that react-bootstrap is a wrapper on top of old school global CSS files as opposed to material-ui which was built from the ground up in React and has a React based CSS-in-JS solution. When people first start learning react, they often slip into bootstrap because its what they know from non-react dev -- but using a framework like React moves the needle and trade offs.
It's not your problem here, but I feel the need to say it :D. At the same time I'd say don't always take a random internet strangers recommendation and refactor for nothing -- you should research it.
A few other important notes:
If id is not set, you set data to an empty string. If that case ever happened for real, your code would error since accessing data.skills would result in undefined. I would argue that you shouldn't even need to handle this case, since your router setup should be such that there is no mapping between this component and a route without an id. Make sure on your router config the id param is non-optional.
You are copying data.skills into a new state item called skills. It's not a bug per-se but it is a code smell as its not actually necessary. Why not edit the data in the data state directly? Copying state that is fundamentally the same is usually an anti-pattern as you can fall into traps where you are trying to keep 2 bits of state in sync that are supposed to be the same. I think its ok where you do it further down as I believe you are copying it because you want that state not to be committed further up until the user clicks save -- which means its actually fundamentally different state at given times. But that doesnt apply to the top level data vs skills one. Actually, I think the inputField state is also not needed since your "staged" state is held by the parent ModalEditSkills in newData. You could instead get rid of this inputField state and have handleChangeInput, handleAddFields and handleRemoveFields call up into ModalEditSkills to patch newData directly. Then pass that down. This will greatly reduce the surface area for bugs and remove unnecessary effects in SkillEdit.
You are often passing props called set[Something]. Generally, in react, you want to keep to the naming convention of user triggered events beginning with on. The trouble with the former, is it implies that the user action does a certain thing, which makes the component look like its less reusable in other contexts (even though its the same behaviour really, its just incorrect naming).
When setting a state item thats derived partly from its previous value, you should use the callback pattern of setState (which is passed the current value) instead of referencing the state value returned from useState. This avoids bugs to do with accidentally getting a stale value when you update state in a loop or something. React doesn't actually update the value of the state immediately, it gets flushed at the end of the call stack. Using the previous value callback gets around this. Not a problem that will show up in your code, just good practice that you should get in the habit of.
There's a bug where you need to reset the form data back to the data stored in the top component when it opens/closes because currently if you make a change and close dialogue, without clicking save, its still there next time you open it. Its not actually saved to the parent state, its just hanging around in the background (hidden). Its weird UX so I've fixed by adding show to the effect that resets the newData state. If you really wanted it how it was though you can just remove that again from the deps array.
There was also a bug in the way you were patching the state when editing an existing skill. Even though you cloned the skills array by spreading a new one, this only does a shallow clone. The object you spread into the array are the same (as in literally, the same object reference) as the original ones. This meant you were mutating the data prop which is not allowed in react. I've changed to Object.assign to make sure its a proper edited clone. The way this works if everything gets merged from right to left. So by the first param being {}, you start with a brand new object and then you load int the old + new data into it. Libraries like Immer make this easier, you might want to look into it.
I haven't fixed this one as it's kind of up to you but if you were on slow network, there would be a period where you could confuse the user since the data hasn't come back yet but they can open the dialogue and see no skills. You might want to handle that case by showing a loading display instead of the rest of the app whilst its in flight. You could make the default state of the top level data state null and then only populate it when the request comes back like you do now. Then in the top level render you'd check for null and return some loading display instead of ModalEditSkills. Similar stuff would happen if the network errored. You might also want to have some new state that says if an error happened (instead of just logging), and check that as well and display the error page.
Heres the code with my proposed changes (minus the library change, you'd still need to do that if you cared enough).
And heres a code sandbox with it working: https://codesandbox.io/s/wispy-meadow-3ru2nq?file=/src/App.js (I replaced the network call with static data for testing, and I dont have your icons).
I hope this feedback helps you on your react journey!
const EditPerson = () => {
const [data, setData] = useState({skills: []});
const { id } = useParams();
useEffect(() => {
const fetchData = async () => {
const docRef = doc(db, "person", id);
try {
const docSnap = await getDoc(docRef);
setData(docSnap.data());
} catch (error) {
console.log(error);
}
};
fetchData().catch(console.error);
}, []);
const handleSkillsChanged = (skills) => {
setData(data => ({...data, skills}));
}
return (
<div>
<ModalEditSkills
onSkillsChanged={handleSkillsChanged}
data={data.skills}
/>
</div>
);
}
const ModalEditSkills = ({ data, onSkillsChanged}) => {
const [show, setShow] = useState(false);
const [newData, setNewData] = useState([]);
useEffect(() => {
setNewData(data);
}, [data, show]);
const handleClose = () => setShow(false);
const handleShow = () => setShow(true);
const handleSkillChange = (index, name, value) => {
setNewData(prevValues => {
const newValues = [...prevValues]
newValues[index] = Object.assign({}, newValues[index], { [name]: value });
return newValues
});
}
const handleSkillAdded = () => {
setNewData(prevValues => [...prevValues, { skill: "", stars: 0 }]);
}
const handleSkillRemoved = (index) => {
setNewData(prevValues => {
const newValues = [...prevValues];
newValues.splice(index, 1);
return newValues
});
}
const handleSubmitSkills = (e) => {
e.preventDefault();
onSkillsChanged(newData);
setShow(false);
};
return (
<>
<div className="content_header">
<div className="content_header_top">
<div className="header_left">Skills</div>
<div className="header_right">
<Button className="round-btn" onClick={handleShow}>
<i className="fa-solid fa-pencil t-14"></i>
</Button>
</div>
</div>
</div>
{show && (
<Modal show={show} onHide={handleClose} size="">
<Modal.Header closeButton>
<Modal.Title>Edit Person</Modal.Title>
</Modal.Header>
<Modal.Body>
<SkillEdit
data={newData}
onSkillChanged={handleSkillChange}
onSkillAdded={handleSkillAdded}
onSkillRemoved={handleSkillRemoved}
/>
</Modal.Body>
<Modal.Footer>
<Form>
<Button
className="btn-skill-complete"
onClick={handleSubmitSkills}
>
Save
</Button>
</Form>
</Modal.Footer>
</Modal>
)}
</>
);
};
const SkillEdit = ({ data, onSkillChanged, onSkillRemoved, onSkillAdded}) => {
return (
<Form>
<div>
{data?.map((inputField, index) => (
<div key={index}>
<Row>
<Col xs={5} md={5}>
<Form.Group as={Col}>
<Form.Control
className="mb-3"
type="text"
id="skill"
name="skill"
value={inputField?.skill}
onChange={(event) =>
onSkillChanged(index, "skill", event.target.value)
}
/>
</Form.Group>
</Col>
<Col xs={4} md={4}>
<div>
<Form.Group>
<ReactStars
type="number"
name="stars"
count={5}
size={24}
id="stars"
onChange={(newValue) =>
onSkillChanged(index, "stars", newValue)
}}
emptyIcon={<i className="fa-solid fa-star"></i>}
filledIcon={<i className="fa-solid fa-star"></i>}
value={inputField.stars}
/>
</Form.Group>
</div>
</Col>
<Col xs={3} md={3}>
<div>
<button
type="button"
onClick={onSkillAdded}
>
<i className="fa-solid fa-plus"></i>
</button>
<button
type="button"
onClick={() => onSkillRemoved(index)}
>
<i className="fa-solid fa-minus"></i>
</button>
</div>
</Col>
</Row>
</div>
))}
</div>
</Form>
);
};
Related
I didn't get data for the first time.
Here is my screen shot when I search for the first time it return undefined and when I search for second time it return proper data.
How to I fix this problem. And please also explain what does it happens. I search this behavior from 2 days but I didn't find any solution even from stack overflow.
Here is my code.
import logo from './logo.svg';
import './App.css';
import Navbar from './components/Navbar';
import { useEffect, useMemo, useState } from 'react'
function App() {
const [searchWord, setSearchWord] = useState('');
const [responseWord, setResponseWord] = useState();
const [isLoad, setIsLoad] = useState(false)
const [urlLink, setUrlLink] = useState('')
async function fetchWord(word) {
console.log(isLoad)
const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`);
const data = await res.json();
setIsLoad(true)
setResponseWord(data)
console.log(responseWord)
console.log(isLoad)
}
return (
<>
<Navbar />
<div className="container mt-4">
<div className="row">
<div className="column bg-success text-light text-center col-3" style={{ height: "100vh" }}>
<h4> English Dictionary</h4>
</div>
<div className="column col-5 bg-light">
{
isLoad &&
<>
<h3 className='word'>{responseWord.word}</h3>
</>
}
</div>
<div className="row col-3" style={{ height: 50 }}>
<form className="d-flex" role="search" onSubmit={(e) => e.preventDefault()}>
<input className="form-control mr-sm-2" placeholder="Search"
onChange={(e) => setSearchWord(e.target.value)}
value={searchWord}
/>
<button className="btn btn-outline-success my-2 my-sm-0" type="submit" onClick={() => fetchWord(searchWord)} >Search</button>
</form>
</div>
</div>
</div>
</>
);
}
export default App;
When responseWord was printed the first time, responseWord's value was not updated to new value. Because setState operates asynchronously.
Use useEffect hook instead.
async function fetchWord(word) {
console.log(isLoad)
const res = await fetch(`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`);
const data = await res.json();
setIsLoad(true)
setResponseWord(data)
}
useEffect(() => {
console.log(responseWord);
}, [responseWord]);
can you try this one please i think will it help you out
import React, { useEffect, useMemo, useState } from "react";
const App=()=> {
const [searchWord, setSearchWord] = useState("");
const [responseWord, setResponseWord] = useState([]);
const [isLoad, setIsLoad] = useState(false);
const [urlLink, setUrlLink] = useState("");
const fetchWord = async (word) => {
console.log(word);
try {
setIsLoad(false);
const res = await fetch(
`https://api.dictionaryapi.dev/api/v2/entries/en/${word}`
);
const data = await res.json();
setIsLoad(true);
setResponseWord(data);
} catch (er) {
setIsLoad(false);
}
};
const HandleSubmit = (e) => {
e.preventDefault();
fetchWord(searchWord);
};
return (
<>
<Navbar />
<div className="container mt-4">
<div className="row">
<div
className="column bg-success text-light text-center col-3"
style={{ height: "100vh" }}
>
<h4> English Dictionary</h4>
</div>
<div className="column col-5 bg-light">
{isLoad && responseWord.length !== 0 && (
<>
{/* <h3 className="word">{responseWord.word}</h3> */}
{responseWord.map((eg, i) => (
<h3 key={i || eg}>{eg.word}</h3>
))}
</>
)}
</div>
<div className="row col-3" style={{ height: 50 }}>
<form className="d-flex" role="search" onSubmit={HandleSubmit}>
<input
className="form-control mr-sm-2"
placeholder="Search"
onChange={(e) => setSearchWord(e.target.value)}
value={searchWord}
/>
<button
className="btn btn-outline-success my-2 my-sm-0"
type="submit"
>
Search
</button>
</form>
</div>
</div>
</div>
</>
);
}
export default App;
don't console inside the Asynce function cause async function will await until responce came so your results will be previous state
and assign your useState intially with empty array that will work properly if in case empty data
Muhammad, you're off to a great start here. First, let's take a look at your code as it is at the moment. Then, I'll make a couple of recommendations on how to refactor your code.
Congrats! You're actually getting data the first time you click the button and trigger fetchWord function.
You're just calling console.log(responseWord) and console.log(isLoad) too early. You're trying to log responseWord and isLoad right after updating their state within the same function. This happens because "calls to setState are asynchronous inside event handlers" and changes to state variables do NOT reflect the new value immediately after calling setState.
"When state changes, the component responds by re-rendering." And it is in the new re-render that the new state value will be reflected.
Why doesn’t React update state synchronously?
According to React documentation, React intentionally “waits” until all components call setState() in their event handlers before starting to re-render. This boosts performance by avoiding unnecessary re-renders.
When you call setResponseWord("new value") and setIsLoad("new value"), and then try to log the new state values to the console before React re-denders your component, you get false and undefined.
Try console.log(data) instead of console.log(responseWord).
Since you have access to const data = await res.json(); inside your function and before the component re-render happens, you should be able to see your data right away.
The images below ilustrate this example (focus on line 16):
Next, recommendations:
1 . It is recommended to make your AJAX call to an API using useEffect Hook.
This way, you can add the serachWord to the dependency array, and useEffect will execute every time the value of seachWord changes.
In your case, you make your fetch call on button click, but if, for example, you created a web app in which you needed the data to be populated right away without the user having to click a button, useEffect Hook will shine at its best because useEffect automatically runs the side-effect right after initial rendering, and on later renderings only if the value of the variables you passed in the dependency array change.
See the image below from the React documentation to get an idea of how you could refactor your code:
Another recommendation is to get rid of the onClick property in the button, and just let the handleSubmit function call fetchWord (see lines 38 and 21-24).
This information should help you move your app forward. And you're doing great. I see that you're successfully updating state variables, using async/await, making AJAX calls using fetch() and learning React.
Please take a look at the reference list below:
https://reactjs.org/docs/faq-state.html
https://reactjs.org/docs/faq-ajax.html
https://reactjs.org/docs/forms.html
I have a modal that opens on click event. It gets the event data (date and time string) and displays it inside the modal. The problem is that my state is always one step behind. The content being displayed by the modal is always in the previous state. When I close the modal and reopen it, It does render the content but It is the previous event. How to solve this issue? am I using the react hooks wrong? I cannot post the whole code as It's quite large so I am just posting the parts where I am using hooks, please let me know if you need more information I will provide it.
const [isModalVisible, setIsModalVisible] = useState(false)
const [modelContent, setModalContent] = useState('')
const [modalTitle, setModalTitle] = useState('')
const handleEventAdd = (e) => {
form.resetFields()
setModalTitle('Add an Event')
const event = _.cloneDeep(e)
form.setFieldsValue({
datePicker: dateString,
timeRangePicker: [dateString, eventEnd.date],
})
}
const content = () => (
<div>
<Form
form={form}
initialValues={{
datePicker: moment(e.start),
timeRangePicker: [moment(e.start), moment(e.end)],
}}
>
<Form.Item name="datePicker" label="Current Event Date">
<DatePicker
className="w-100"
format={preferredDateFormat}
onChange={setValueDate}
/>
</Form.Item>
<Form.Item>
<Button
onClick={() => {
form
.validateFields()
.then((values) => {
form.resetFields()
})
}}
>
Add Event
</Button>
</Form.Item>
</Form>
</div>
)
setModalContent(content)
setIsModalVisible(true)
}
const handleModalClose = () => {
setIsModalVisible(false)
form.resetFields()
setModalContent('')
}
UPDATE::
my return function consists,
<Modal visible={isModalVisible} footer={null} onCancel={handleModalClose} title={modalTitle}>
{modelContent}
</Modal>
This problem is due to the asynchronous behavior of setState. This method generates a "pending state transition" (see this for more information). In order to solve this issue, you have two options:
use ref instead of state. In contrast to state, ref has synchronous behavior. I have developed an example for myself that you can check out here. you can see that there is an asynchronous behavior in the state (dialog box vs. in webpage).
You can explicitly pass the new state value as part of the event being raised.
I have a form within a modal and I want to, on Update Info button click, take the data from that form and have them in a list as the value on a hidden input that is outside the modal so that I can save the info together with the rest of form inputs. Here's the code I have so far for the modal section, I would really appreciate it if someone would point me to how I can go about this.
const modalFormInput = ({id, value}) =>
<div className='col-sm-3'>
<Button onClick={handleShow}>
Edit Personal Info
</Button>
<Modal show={show} onHide={handleClose}>
<Modal.Header closeButton>
<Modal.Title>EDIT PERSONAL INFO</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row key={id} className={'form-group'}>
<FormText model={model}
path={path(id, 'name')}
label={Official Name}
value={value}
error={state.nameErrors[id]}
/>
</Row>
</Modal.Body>
<Modal.Footer>
<Button>
Update Information
</Button>
</Modal.Footer>
</Modal>
</div>
I used React Docs to do the modal, I'm very new to React so any help will be greatly appreciated.
I'm not sure what follows is helpful.
I'd need a bit more surrounding context to provide a complete answer (and your example seems to be missing some critical information, e.g. where handleShow and handleClose come from), but I can try to illuminate a common pattern for this kind of thing.
Summarized: Pass an event handler function as a prop and have the component call that function to communicate information back to the parent component.
Here's a contrived example of the basic pattern.
It renders an input with an onChange handler and some text displaying the current value.
When the user types in the input, the input invokes the handler with an event object. (This is built-in behavior)
The onNameChange handler updates the component's state with the value from the event.
The new value is reflected in the text on the subsequent render. (Updating state triggers a re-render, so this happens automatically.)
function SomeComponent () {
// keep track of the name in state so we can use it for other stuff
const [name, setName] = React.useState('');
// define a change event handler for the input
const onNameChange = event => {
// event.target is the input
// event.target.value is the new value
const newValue = event.target.value;
// update state with the new value
setName(newValue);
}
return (
<div>
<input value={name} onChange={onNameChange} />
<span>Hello, {name}</span> {/* updates as the user types */}
</div>
)
}
Extending this to more closely resemble your example, SomeComponent could accept an event handling prop of its own to be invoked when a button is clicked:
function SomeComponent ({onButtonClick}) { // accepts an 'onButtonClick' prop
// same as before
const [name, setName] = React.useState('');
// same as before
const onNameChange = event => {
const newValue = event.target.value;
setName(newValue);
}
return (
<div>
<input value={name} onChange={onNameChange} />
{/* call the handler with the current value of name */}
<button onClick={() => onButtonClick(name)}>Click Me</button>
</div>
)
}
With that in place, a different component can now use SomeComponent to do something with name when the button is clicked:
function SomeOtherComponent () {
const buttonClickHandler = (name) => alert(name); // or whatever
return (
<SomeComponent onButtonClick={buttonClickHandler} />
);
}
So, I was refactoring a React component after reading more about React principles of lifting state up and in particular after reading this about derived state.
I had a component like so (before the refactor) that was working but now I realize was duplicating state among multiple sources of truth (in the parent and the child):
const ChildComponent = ({
prevStep,
nextStep,
parentStateData,
updateParentStateData,
}) => {
const [childState, updateChildState] = useState(
parentStateData["thisChildData"]
);
const updateParentStateDataFromChild = () => {
return updateParentStateData(
Object.assign(parentStateData, { thisChildData: childState })
);
};
useEffect(() => updateParentStateDataFromChild(), [childState]);
return (
<div className="text-center">
<h1 className="step-title">Choose Child State</h1>
<div class="state-size-buttons">
<Button
onClick={() => updateChildState(1)}
active={childState === 1}
className="mt-3"
variant="light"
size="lg"
>
1
</Button>
<Button
onClick={() => updateChildState(2)}
active={childState === 2}
className="mt-3"
variant="light"
size="lg"
>
2
</Button>
<Button
onClick={() => updateChildState(3)}
active={childState === 3}
className="mt-3"
variant="light"
size="lg"
>
3
</Button>
</div>
<Button onClick={() => prevStep()} className="mr-2" variant="light">
Go Back
</Button>
<Button disabled={!childState} onClick={() => nextStep()} variant="light">
Next
</Button>
</div>
);
};
Again, this worked but it's not good (I think) because it duplicates state. The ChildComponent receives the parent state (parentStateData) and an update function for that state (updateParentStateData) but duplicates that state in its own childState and updateChildState - which in turn updates the parent using a useEffect() hook -> when child state updates, update the parent state.
So, I started my refactor, like this:
const ChildComponent = ({
prevStep,
nextStep,
parentStateData,
updateParentStateData,
}) => {
const updateState = (data) => {
return updateParentStateData(
Object.assign(parentStateData, { thisChildData: data })
);
};
const nextDisabled = !parentStateData["thisChildData"];
return (
<div className="text-center">
<h1 className="step-title">Choose A State</h1>
<div class="state-size-buttons">
<Button
onClick={() => updateState(1)}
active={parentStateData["thisChildData"] === 1}
className="mt-3"
variant="light"
size="lg"
>
1
</Button>
<Button
onClick={() => updateState(2)}
active={parentStateData["thisChildData"] === 2}
className="mt-3"
variant="light"
size="lg"
>
2
</Button>
<Button
onClick={() => updateState(3)}
active={parentStateData["thisChildData"] === 3}
className="mt-3"
variant="light"
size="lg"
>
3
</Button>
</div>
<Button onClick={() => prevStep()} className="mr-2" variant="light">
Go Back
</Button>
<Button
disabled={nextDisabled}
onClick={() => nextStep()}
variant="light"
>
Next
</Button>
</div>
);
};
This time as you can see, there is no internal state and the parent state object simply gets updated at the relevant key using Object.assign().
But my problem is these state changes never get reflected in my view. If I do a React component inspector I see the state is changing (although it seems to lag) but the view doesn't change. Most critically, the nextDisabled constant and variants of that (I tried putting that logic directly into the Next button <Button disabled={nextDisabled} onClick={()=>nextStep()} variant="light">Next</Button>) don't ever change. So the parent state gets updated, but !parentStateData['thisChildData'] never changes value or never runs again... so what gets rerendered when the props (via the change in parentStateData) change? Why doesn't the simple validation on the next button work as I expect when I update the parent state? That is, once I select a number using the buttons (no longer 0 from the default) I would expect it to not be disabled (example: !1 is false - so not disabled as its for the disabled prop).
But it never changes!
It stays evaluated to true despite the state updating in the parent and therefore the props changing in the child. So somewhere I have a conceptual error. Any help here would be very much appreciated.
React, and most of the tools working with it, are based on the fact that states are immutable. It means that if you want to update a state, you must not change values in the same object, but you need to provide a new object.
So, the following is an error, because it updates the value of the state:
const updateState = (data) => {
return updateParentStateData(
Object.assign(parentStateData, { thisChildData: data })
);
};
Instead, you should create a new object, for example:
const updateState = (data) => {
return updateParentStateData(
Object.assign({}, parentStateData, { thisChildData: data })
);
};
I think that your first example is working only because it really updates the child property.
I am trying to create my first search bar in React.js. I am trying to implement search functionality with filter method. I faced a problem with filter method, which gives an error like "filter is not defined". I am stuck on it for 2 days, I have looked several tutorials and endless youtube videos. This is the simpliest approach, I guess. Any help will be appreciated.
import React, { useState, useEffect } from "react";
import Recipe from "./Recipe";
import "./styles.css";
export default function RecipeList() {
const apiURL = "https://www.themealdb.com/api/json/v1/1/search.php?f=c";
const [myRecipes, setRecipes] = useState("");
const [search, setSearch] = useState("");
// fetch recipe from API
function fetchRecipes() {
fetch(apiURL)
.then(response => response.json())
.then(data => setRecipes(data.meals))
.catch(console.log("Error"));
}
function onDeleteHandler(index) {
setRecipes(
myRecipes.filter((element, filterIndex) => index !== filterIndex)
);
}
useEffect(() => {
fetchRecipes();
}, []);
const filterRecipes = myRecipe.meal.filter( element => {
return element.name.toLowerCase().includes(search.toLocaleLowerCase())
})
{/* filter method above doesn't work */}
return (
<div>
<label>
<div className="input-group mb-3 cb-search">
<input
type="text"
className="form-control"
placeholder="Search for recipes..."
aria-label="Recipient's username"
aria-describedby="button-addon2"
onChange = {e => setSearch (e.target.value)}
/>
<div className="input-group-append">
<button
className="btn btn-outline-secondary"
type="button"
id="button-addon2"
>
Search
</button>
</div>
</div>
</label>
<div>
<button
className="btn btn-info cb-button fetch-button"
onClick={fetchRecipes}
>
Fetch Recipe
</button>
<br />
{filterRecipes.map((element, index) => (
<Recipe
key={index}
index = {index}
onDelete={onDeleteHandler}
{...element}
name = {element.strMeal}
/>
))}
{/** name of child component */}
{/** strMeal is the name of Recipe in API object */}
</div>
</div>
);
}
link for code codesandbox
I made some changes on your code updated code
const [myRecipes, setRecipes] = useState([]);
You should declare myRecipes as an array if u intended to use map function.
const filterRecipes = myRecipe.meal.filter( element => {
return element.name.toLowerCase().includes(search.toLocaleLowerCase())
})
You have the wrong variable passing through, it should be myRecipes
filterRecipes.map((element, index) => (
<Recipe
key={index}
index = {index}
onDelete={onDeleteHandler}
{...element}
name = {element.strMeal}
/>
3. You should check whether your filterRecipes is not undefined before you use map function.
Lastly, your fetch API return error which unable to setRecipes.
I could not resolve you task completely because of low count of information according the task, but, i think, my answer will be useful for you.
So, tthe first thing I would like to draw attention to is a initial state in the parameter of useState function. In this task it sould be as:
const [myRecipes, setRecipes] = useState({meals: []});
Because, before fetching data, React has a time to run the code, and, when it come to line 32, it see what in the myRecipes (myRecipes, not a myRecipe. Please, pay attention when you write the code) a string except an array.
And in the line 32 i recommend you to add something checking of have you resolved request of data like:
const filterRecipes = myRecipes.meals.length
? myRecipes.meals.filter(element => {
return element.name.toLowerCase().includes(search.toLocaleLowerCase());
});
: []
And look in the data which you receive, because, i think, there are no elements with propName like name (element.name).
I think, i could help you as possible. If you have any questions, ask in comments. Will answer you as soon as possible. Good luck