I'm creating an app wherein a user can create a set of flashcards with a term and description each. I also want the flashcards to be editable as soon as they add a flashcard. So each flashcard they add gets pushed to a variable with a controlled state. Then I created a component that returns inputs with all of the flashcard data.
this is what it would like look like:
There's no problem with that it works fine. But I want them to be editable once added. So I added an onchange to each of the inputs and did this:
function handleChange(event){
const {value, name, dataset} = event.target
const index = dataset.index
const newFlashcardSet = [...flashcardValues]
newFlashcardSet[index][name] = value;
setFlashcardValues(newFlashcardSet)
}
so when the user tries to type on the submitted flashcard they can edit it.
PROBLEM
As you can probably tell, each key keystroke calls the function, and rerenders all the data. Causing it to eat up a lot of cpu usage, even though it is not noticeable at first, but as soon as you add more flashcards, you would definetly notice the slow performance, because it rerenders all the data every keystroke. What is the best way to go around this?
Related
I was doing some coding on React, and encountered an issue I would like to properly deal with. Details on the matter are provided below.
The Environment
Suppose you have a component FormDemo made to handle a potentially complex form, parts of which involve the dynamic management of certain input fields. As an example, the provided code sample allows to create any amount of fields for names between 0 and (232 - 1) fields due to JavaScript's limitations on array length.
Press Add New Name button above all name fields to append another name field. Press Remove button to the right of any input to delete it from the list.
Each name input created is handled by a separate component SubForm that takes three properties:
id: a unique generated identifier of the current field.
onChange: a function executing whenever the value of that input was changed.
onRemove: a function executing whenever the Remove button of that form was clicked.
The Sample
Here is a working sample of a code I've made on CodeSandbox provided for demonstration purposes.
The Problem
The approach used in the code sample works, but it has the eslint problem mentioned in Problems tab of CodeSandbox, and I am aware that it's not a CodeSandbox issue, as I've tested the same project in my local environment and got the same problem. Here are that problem's details taken right from the console:
React Hook useEffect has a missing dependency: 'onChange'. Either include it or remove the dependency array. If 'onChange' changes too often, find the parent component that defines it and wrap that definition in useCallback. (react-hooks/exhaustive-deps)
Following the advice from the problem directly (i.e. adding onChange to dependency list of SubForm's useEffect) results in infinite rendering, and thus is not a solution to the problem.
The Research
After some reading of the official React docs on useCallback, as well as the other part of these on useEffect, I've figured out that, when rendering a component, React creates new instances of functions declared in a component's body. Therefore, adding such functions to a dependency list of some useEffect hook that has an effect function attached to it will entail that function being called on each render.
In my approach, I pass update function to SubForm component in onChange property as a reference (proven here by React docs), hence the SubForm component's onChange property has exactly the same instance of the update function as the parent component. So, whenever the instance of the update function changes with it added to the dependencies of a useEffect hook, that executes the effect function attached to it, and, taking the above into account, this happens on each render of a parent component FormDemo.
The update function changes the value of forms, a state variable of FormDemo component, causing it to rerender. That recreates the instance of an update function. The SubForm component gets notified of that change and executes an effect function attached to a useEffect hook, calling the update function once again. In turn, this causes another change of a state variable forms, telling the parent component FormDemo to render again... and this continues indefinitely, creating an infinite loop of renders.
Some of you may ask why does it happen if an input field of the form was not changed between these two renders, thus the value passed to update function is effectively the same as before. After some testing, which you can try yourself here, I came to the conclusion that it's actually wrong: the value set to forms is always different. That's because even though the object's content is exactly the same, its instance is different, and React compares object instances instead of their contents, sending a command to rerender the component if these instances differ.
As useCallback hook memoizes the instance of the function between renders, recreating the function only when the values or instances of its dependencies change, I've assumed that wrapping update function in that hook will solve the original problem, because the instance of the function will always stay the same.
However, wrapping the update function in useCallback will result in another problem: I will need to add forms as a dependency, because I'm using it inside that function. But, taking the above into account, this will bring me the original problem back due to the instance of forms being different after each update, and that will command useCallback to recreate the instance of the function, too.
Potential Solution
With all that being said, I have a solution that I don't quite like, even though it works because it removes the need of adding the state variable forms to the list of dependencies of useCallback:
const update = useCallback((id, value) => {
setForms(prevState => {
const { form_list } = prevState,
new_forms = [...form_list],
mod_id = new_forms.map((e) => e.id).indexOf(id);
new_forms[mod_id] = value;
return { ...prevState, form_list: new_forms };
});
}, []);
So why am I against it, if it works and gives no problems in the console?
In my humble opinion (feel free to prove me wrong), because of these issues:
Direct usage of state setter function instead of a dedicated middleware function. This decentralizes direct state management.
Duplication of an original array, which may be expensive on memory if an array has a lot of values inside, not to mention that each value itself is an object.
The Question
What is the most memory-efficient and readable solution of the stated problem in the provided case that will use a middleware function setField? Alternatively, if it's possible to debunk my issues with a potential solution, prove that it's the best way to go.
Feel free to modify the contents of setField if necessary for the solution and remember that I'm all open for answering anything related to the question.
It seems you are duplicating state of each SubForm: you store it in parent and also in SubForm, why not store state only in parent and pass as props?
I am talking about something like this:
const SubForm = ({ id, form, onChange, onRemove }) => {
return (
<Form>
<Form.Group controlId={`form_text${id}`}>
<Form.Label>Name (ID {id})</Form.Label>
<InputGroup>
<Form.Control
type="text"
value={form.name}
onChange={(e) => onChange(id, { ...form, name: e.target.value })}
/>
<Button variant="danger" onClick={() => onRemove(id)}>
Remove
</Button>
</InputGroup>
</Form.Group>
<br />
<br />
</Form>
);
};
To pass each form data just do:
<SubForm key={e.id} form={e} id={e.id} onChange={update} onRemove={remove} />
No need for useEffect anymore.
You probably want to separate the management of IDs from SubFrom. SubForm shouldn't be able to change it's ID.
Wrap the update & remove functions - so SubForm doesn't need to send the ID back.
<SubForm key={e.id} id={e.id}
onChange={(form) => update(e.id, form)}
onRemove={() => remove(e.id) } />
Make sure that SubForm will not change ID as part of form
const update = (id, value) => {
setField(
"form_list",
// subform shouldn't change id, so we overriding it (to be sure)
forms.form_list.map(e => e.id===id?{...value, id}:e)
);
};
You still optionally may pass the ID to the SubForm, but the management of IDs is separated from it.
Modified code
I am building a dynamic form and as of now, my component hierarchy is as follows:-
App
Caseform
DynamicFormBuilder
Context.Provider
Patients
Patient key = "patient_1"
ComponentCreator key = "patient_1_1"
Textbox
ComponentCreator key = "patient_1_2"
Textbox
ComponentCreator key = "patient_1_3"
Textbox
Patient key = "patient_2"
ComponentCreator key = "patient_2_1"
Textbox
ComponentCreator key = "patient_2_2"
Textbox
ComponentCreator key = "patient_2_3"
Textbox
As of now, I have hardcoded JSON data in caseform but eventually, it will come from the fetch call. As soon as DynamicFormBuilder receives the caseform metadata, it creates states out of it.
I am maintaining state at caseform level even for its child. I thought of doing it that way because the requirement was to support patch save(send only changed data to the backend on save button press). If anybody knows of a better way of doing this, please let me know.
I am passing in a function using context API to child components so that they can update the state in DynamicFormBuiler.
The issue I am facing is that let's say even if the user edits one textbox, the whole Dynamic form gets rendered.
I have gone through a lot of answers on StackOverflow that advise on using the shouldComponentUpdate lifecycle method, but I am not able to figure out how I will use it here.
I am adding a link to the sandbox and in the console, you can see that if a user edits a field all the things are rendered again.
CodeSandbox Link
Expected Behavior:- What I am looking for is for example:- if user-edited only one textbox say in patient 1 then only that textbox gets re-rendered again
This is my first time using react. I apologize in advance if someone feels I have not done enough research, I have read through a lot of questions but still facing some challenges, any help will be really appreciated.
If you wrap each component in the React.memo() HOC, this should prevent excessive rerenders.
https://reactjs.org/docs/react-api.html#reactmemo
-Edit-
Yes it works with class components too.
e.g.
const MyComponent = React.memo(class extends React.Component {
render () {
return <h1>TEST</h1>
}
});
This is more of a hypothetical question as I would like to have a clear idea before trying to write the code for this problem.
As an example, lets say I have Board (parent) and Card (children) components.
The Cards are placed inside the Board and contain some text elements inside (maybe Todo items) and can be moved around inside the board and from board to board. Thus, state is used to allow users to move the cards from board to board, then "check-off" a todo item, and not reset everything to original positions.
But what happens if I want to persist after a user reloads the page?
Is my only option to store everything in localStorage after "stringifying", or are there other (better) alternatives?
I see a lot of examples online with a single component where you simply store the state and text of that component, but that seems very inefficient and complex when it comes to components with children.
The usual way is using localStorage. A full example of how localStorage works, you can change the states with your own.
const LOCAL_STORAGE_KEY = 'todosvar'
useEffect(() => {
const storedTodos = JSON.parse(localStorage.getItem
(LOCAL_STORAGE_KEY))
if (storedTodos) setTodos(storedTodos)
}, [])
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(todosvar))
}, [todosvar])
So what I want to achieve is: 1.Press on the button 2.It toggles its state(I made a toggle function). 3.It sends the current state to database. 4. Record with button's state changes 5. I close the app, run it again and it gets the data with the current state of button.
So now comes a question: How can I do that? I didn't really find anything useful to make that specific connection database-frontend.
// There is a lot of code which I dont want to spam with so make it simple: I already GET data from database and the BNS-H11 you can see its the result of it.
I call it with componentDidMount().
I passed props to the components.
//ISLbutton.js < button component
const [name, setName] = useState(props.islName),
const [islIsOn, setIslIsOn] = useState(props.islIsOn)
// functions for auth check and toggle...
Update: Here is an example Pen
https://codepen.io/anon/pen/vwzGYY?editors=0011
Preface
Based on my research, it seems like I need a completely different approach. Maybe you can suggest one?
Context
I'm using a Redux-Form (technically an older version, but the API's
in question seem really stable. We can burn that bridge when we get there.) to set some "filters" for a sort of search results list.
In particular, since I want the pages to be link-able, I'm also setting the form content in the URL query params, via React-Router, or initially setting it on page load via similar mechanism.
The only field so far is "organization_name", a text field, used to set the query param value, and trigger an API request for /endpoint?name={some_name}.
E.g.,
<Field
name="organization_name"
component="input"
type="text"
placeholder="Organization Name"
value={value}
/>
I've tried several things, but here's a recent shot:
I'm grabbing reset, change, and other things from default props. I'm passing in a handleSubmit as required.
handleSubmit works correctly, to do some state updating, set/push the URL query params with React Router, and then make a new API call/update display of new results! Woot!
What I want / expect
In the long run, I would like a "reset filters" button that sets all filter values back to defaults (e.g., set the "name" value to empty string), and re-submits the form (thus triggering handleSubmit).
What I first tried to implement was a button, as such:
<button
name="reset_filters_button"
type="button"
onClick={resetAndSubmit}
disabled={pristine || submitting}
>
Clear Search
</button>
Where resetAndSubmit is defined on the form container as such:
const resetAndSubmit = event => {
reset();
handleSubmit();
};
What actually happens... (submit takes precedence over dispatched events?)
Using the Chrome dev tools debugger, I can clearly see that the reset method is called, and returns it's dispatch(...)'d event. However, the form and state values are not updated before handleSubmit() runs and submits the form.
I think this might have to do with the submit event taking priority?
I have also tried something janky, like importing change (default prop for the container) and defining the reset button thus:
<button
name="reset_filters_button"
type="button"
onClick={() => {
change('organization_name', '');
methodThatDispatchesSubmitAction();
}}
disabled={pristine || submitting}
>
Clear Search
</button>
Which (if I remove methodThatDispatchesSubmitAction()) works correctly to set the field value back to blank, making the form technically "pristine" again as well.
methodThatDispatchesSubmitAction() (if it's not obvious) is bound on the parent via dispatchToProps, and passed in to the form container, where it uses the "remote submit" suggestion, e.g,
// organization_list_filter == name of the Redux-Form to submit.
dispatch(submit('organization_list_filter'));
TL;DR and final question:
How does one properly reset a form and submit its' default/empty values?
Every time I dispatch or directly call Redux Form 'submit', it ends up submitting the form before clearing values from state, or the UI. I have walked through this with a debugger and it's not skipping my call to reset or change. It's like an async/race issue, but I admit I am out of my league in this particular case for sure.
Am I just Straight Up Doing It Wrong?
It is most definitely a race condition issue (or since we aren't actually dealing with threads, an order of events issue).
The reason using a methodThatDispatchesSubmitAction works when your current example does not, is because a dispatched action has the benefit of reading data directly from the redux store. Your example is not reading from the redux store, it's reading from a property that is passed in. Yes, this property comes from the redux store, but the problem you are seeing is that it hasn't been updated in your component yet.
Bear with me as this next piece is not going to be entirely accurate but it should suffice to explain what you are seeing.
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is fired using values prop (old state data still)
Component is updated with new props from redux state
As you can see, the order of events don't allow for an updated state to be given to the property until our click code has finished running. If you've ever watched a video on the JS Event Loop (I highly recommend it), you'll know that our onClick handle will run in full before any other async operations (or sync operations that come after our click) have a chance to run.
There are good reasons why Components aren't given updated props right away but the primary one is performance. You can see that this order is in fact the problem by wrapping the handleSubmit in an async event that fires immediately (it doesn't actually fire immediately, all other sync/async operations queued before it will finish).
const resetAndSubmit = (event) => {
reset();
setImmediate(() => handleSubmit());
}
This changes the order of events as follows:
Submit is clicked
-> Reset action is dispatched
-> Reducer receives action and returns updated state
-> Handle submit is queued on the event loop (not run yet)
Component is updated with new props from redux state
Event loop reaches queued code and runs is
-> Handle submit is fired using values prop (new state data)
Hopefully, this helps you understand why the problem is occurring. As for solutions to fix it. Obviously, you can queue the handle submit as I've shown above. Another option would the one you've described as using a dispatch to perform the submit. A third option would be to use something a bit heavier like redux-thunk or redux-sagas that tie the resetAndSubmit action into a single dispatch. Although honestly, this is the same as option two, just reduced into a single dispatch. Option four, don't use redux for all your data. Obviously, this fourth option comes with trade-offs but my point being, just because you are using redux in a project doesn't mean every single piece of data needs to be in redux. Though it completely defeats the purpose of redux-forms.
I should also add, you are not alone in being confused by this. When you introduce redux, it messes with how you traditionally think about working with code. Normally you think, I do A then B. But with redux, you do A, wait for A's changes to make it through the system, and then you do B. That's where Sagas or Thunks can be nice. You move more logic to the store to act on the dispatch rather than wait for it to all make its way back down to a component via props.