React + Redux: Changing input focus programatically - javascript

I have a react application with a 'yes/no' question interface. I'm using redux to manage state.
The app presents a series of input fields, which are added dynamically.
When a question is answered with a 'y' or 'n' keystroke, I want the next input in the series to get focus automatically -- enabling fast data-entry. This is proving surprisingly difficult!
My redux store contains the current question's index - I want this to translate into focus on that input.
/*Input Component*/
const quizQs = ({
questionArray = ["Q1", "Q2", "Q3", "Q4"]
currentQIndex, //From Store
changeQIndex, //Action
}) => {
const _handleKeyDown = (e) => {
if(e.key == 'y' || e.key == 'n'){
//Dispatches Action that increases current currentQIndex'
}
}
//_handleFocus()... for updating currentQIndex if an input is focused by the user
return (
{questionArray.map((q, index) => {
return(
<input
key={index}
onKeyDown={_handleKeyDown}
onFocus={_handleFocus}
type="text"
placeholder={q}
/>
)
})}
)
}
/*Main Component -- Connected to Store*/
class myQuiz extends React.Component {
constructor(props){
super(props);
}
render(){
return(
<div>
<quizQs
currentQIndex = {this.props.currentQIndex}
changeQIndex = {this.props.changeQIndex}
/>
</div>
)}
}
I have tried setting autoFocus = true, if the store's 'currentQIndex' matches the index of that particular question, in the 'quizQs' component. This method is able to focus the specified field when the page first renders, but the focus does not change when the 'currentQIndex' of the store changes.
As I have searched for an answer, React 'refs' + use of a callback would seem to be the way to go, (https://reactjs.org/docs/refs-and-the-dom.html#the-ref-callback-attribute), but I cannot figure out how to set up such 'focus' callback, that responds to changes in the Redux store.
In addition, in order to use Refs, the component must be set up as a class, not an arrow function. AFAIK, it is not good practice to have multiple classes in one file, and it does not seem appropriate to connect so many different components to a redux store.
I'd appreciate help.

Here is simple example of what you are trying to achieve: https://codesandbox.io/s/z64qw3nkzx
I simplified it a bit, but the point is there.
As .focus() is native method on the DOM element, you need a way of tracking those input elements. For that in React there is ref prop. It accepts a function with has one parameter which is the actual DOM element of the component.
You'll see that I put all of the DOM references into an array:
<input
ref={el => this.questionInputElements[index] = el}
key={index}
// all other props
/>
and on key up* find the next element in the array and focus it:
const nextInput = this.questionInputElements[index + 1];
if (nextInput) {
nextInput.focus();
}
* it needs to be on key up (rather than key down) as it would focus the next field before and print y/n in the next input. Try it for fun :)

Related

Javascript (React) not dynamically displaying collection

I'm trying to create a basic multi-stage web form in Javascript. I wanted to accomplish this kind of like the forms section of Full Stack Open, by creating a collection (an array) of questions, then displaying them as labels on my web page, filtered so that only the appropriate ones appeared at certain times. For example, when you first visit the page, it would say "The next few questions will assess your budget situation", then after pressing start - it would transition to the first question, then pressing next, to the second, and so on.
I thought I accomplished this the correct way, by displaying the filtered collection (phase is initialized to 0 outside of the app):
const questionPhase = () => {
if (phase < 3){
phase = phase + 1;
}
else if(phase == 3){
phase = 0;
addToBudget(attribute);
}
console.log(phase);
}
return (
<div>
<h3> Please answer some questions to start</h3>
<ul>
{questions.filter(question => question.phase === phase).map(question =>
{ < label > {question.script}
<input type="text"
question = {question.attribute}
value={question.attribute}
onChange={handleInput}
/>
</label>})}
</ul>
<button onClick={questionPhase}> {phase === 2 ? 'Next' : 'Submit'} </button>
</div>
)
I've done some logging and determined that phase actually is changing every time I press the button at the bottom. But what doesn't happen, is either the questions (and labels) displaying, or the lettering on the buttons changing.
I'm not sure what I've done wrong? I'm certain there's some subtle aspect of the control flow I've missed but I don't know what - I figured that, as explained in FSO, the app is continually being run through every time there's a change by pressing a button or something, which should be the event created by the button press.
Thank you in advance for any help
appendix: here is the questionsList class (from which questions is imported) and the event handler:
import React from 'react'
const questions = [
{
phase: 1 ,
script: 'What is your monthly income?',
attribute: "income"
},
{
phase: 2,
script: 'what are you monthly expenses?',
attribute: "expenses"
}
]
export default questions
const handleInput = (event) => {
const value = event.target.value;
console.log('valued!')
setAttribute({
...attribute,
[event.target.question]: value
})
}
The only thing that will trigger a re-render in React is a change in state, so if a variable's change should cause a re-render, you should stick it in state.
You can change your questionPhase component to a class or function (same thing), and then in the constructor, define
this.state = {phase};
Then this.state.phase will equal whatever phase was when the component instantiated. You'll want to change the rest of the component to use that variable. The correct way to change its value and trigger a re-render is with a call to setState.
That's the way I would do it; although, it would be easier to just call forceUpdate. That will make react re-render. It's not the react way though. The entire purpose of react is to strongly tie the UI to the underlying state, so I wouldn't recommend using forceUpdate.

Can the child component receive old data for manipulation from parent component?

Let's define a Tags component (a fancy checkbox group).
const Tags = ({ tags, selectedIds, onSelectionChange }) => {
const createClickHandler = (id) => () => {
const newSelectedIds = xor(selectedIds, [id]);
const selectedTags = newSelectedIds.map((id) =>
tags.find((tag) => tag.id === id)
);
onSelectionChange(selectedTags);
};
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={createClickHandler(id)}
>
{text}
</button>
))}
</div>
);
};
This allows us to consume it like this:
export default function App() {
const tags = someUsers.map((user) => ({
id: user.id,
text: user.name,
value: user
}));
const [selectedTags, setSelectedTags] = useState([]);
const selectedIds = selectedTags.map((tag) => tag.id);
return (
<div>
<Tags
tags={tags}
selectedIds={selectedIds}
onSelectionChange={setSelectedTags}
/>
</div>
);
}
You can test this in https://codesandbox.io/s/musing-goldwasser-nmm13
I believe this is a decent design of a component and its props (the main focus is on the ease of consuming for the other components). We could perhaps remove selectedIds and add a selected flag in the tags prop, however this is beyond the question scope.
My colleague on the other hand insists that this can lead to bugs and should be avoided.
The reasoning is as follows: if we want to update the state we must use appropriate API - setState(oldState => //data manipulation to produce new state) from useState (https://reactjs.org/docs/hooks-reference.html#functional-updates)
Since the parent passes the state directly to the children we can't be sure that the child component filters data based on the latest data. Basically, this issue: https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value
His implementation would be something along these lines:
const Tags = ({ tags, selectedIds, onTagClick }) => {
const isSelected = (id) => selectedIds.includes(id);
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={() => onTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
In this case, we lift the whole filtering to a parent component
const handleTagClick = (id) =>
setSelectedTagsIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
You can test this in: https://codesandbox.io/s/kind-cdn-j7cg3
or another version:
const Tags = ({ tags, selectedIds, setSelectedIds }) => {
const isSelected = (id) => selectedIds.includes(id);
const handleTagClick = (id) =>
setSelectedIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
return (
<div>
{tags.map(({ id, text }) => (
<button
key={id}
type="button"
style={{ backgroundColor: isSelected(id) ? "gray" : "white" }}
onClick={() => handleTagClick(id)}
>
{text}
</button>
))}
</div>
);
};
in this case, we leave the filtering to the Tags component however we pass the function which allows modification of state based on old state.
You can test this code in https://codesandbox.io/s/relaxed-leftpad-y13wo
In my opinion, this case is a completely different scenario that React docs never specifically address.
As far as I understand React rendering engine will always ensure that the child nodes get the newest props so a situation where a child component filters (or does other manipulation) with stale data is simply impossible. I would like to quote some docs for this however I haven't found any information on this specific situation.
All I know is:
with my many years of React experience I have yet to encounter any bugs with my approach
other 3rd party libraries use the same design
Can someone (with deep React knowledge) provide more insight why I am correct or wrong in this instance?
For you to notice the difference, you could simulate a delay in the update of the selection. i.e., the user of your component needs to do some async stuff when selecting a tag
const [selectedTags, setSelectedTags] = useState([]);
const selectedIds = selectedTags.map((tag) => tag.id);
const asyncSelection = (tags) => {
setTimeout(() => setSelectedTags(tags), 1000);
};
...
<Tags
tags={tags}
selectedIds={selectedIds}
onSelectionChange={asyncSelection}
/>
You can try here clicking each option one by one, and when all updates run, not all options will be selected (which is not expected). Since the component didn't render immediately, the handler was not updated and the second click is executed with an old state, therefore, the sequences of the selections are not correctly synced. Of course, this is a contrived example, but it could be the case in a very heavy UI that 2 clicks happen without the Tags component being rerendered.
On the other hand, letting the user have more control over the state would be possible to handle this situation. Once again, if you try here clicking each option one by one, in the end, all will be selected as expected
const handleTagClick = (id) => {
setTimeout(() => {
setSelectedTagsIds((oldIds) => {
if (oldIds.includes(id)) return oldIds.filter((oldId) => oldId !== id);
return [...oldIds, id];
});
}, 1000);
};
As far as I can see the things you're talking about are two different issues.
If the props of a child are updated it will trigger a rerender of that component. There are edge cases where that gets tricky like with useRef or some callbacks but that's besides the point. The filtering and things you're doing will never be different or affected in any way as long as it's dependent on the props changing and if the component receives new props it will rerender the child and reapply the filters without any issues.
The second issue is sort of different from the first one. What could happen is that the tag state is repeatedly updated and only one of those states are passed to the child, that's what you want to avoid. Essentially you have to make sure the parent state has actually updated correctly before passing it to a child. The child will always update and filter and do everything correctly exactly on what's passed to it, your problem here is making sure you're actually passing the correct props.
There's no need to move anything to the parent component, the child will update itself correctly when the parent tag state updates and passes that new state to the child, the only thing you have to look out for here is that you don't update the parent state multiple times and cause https://reactjs.org/docs/faq-state.html#why-is-setstate-giving-me-the-wrong-value and end up passing the wrong props to the child. For example if someone spams the group checkbox on and off quickly. Even then if you pass the wrong props to the child the child will still update itself and reapply the filtering and everything, just on the wrong props.
React will do its batch state update on something like a 10ms interval (I'm not exactly sure how long it is). So if someone clicks the checkbox and it updates the tag state at 6/10ms it will rerender the component 4ms later when it does the batch state update. If hypothetically during those 4ms you click it off again, or if straight after it updated you click it off again, it's where weird things start happening. This is why if you use the increment counter (like in that example) multiple times it won't actually increase it by 3, only by 1, since the code will execute all 3 times on 0 before it did the state update 10ms later. That being said even if you spam that checkbox on and off all the time (spamming the tag array state), I don't see any way how it would go out of sync, every 10ms it will update and rerender the child and the moment you stop spamming it the child will finally rerender on the last current parent state and be correct. I don't see how you could really have an issue with that in your example. It could cause an issue with something like a counter but not with your tags because of the fact that a counter is a cumulative addition on previous values whereas your tags is a static set of values (that is the key difference).

Reactjs: how to write a method to handle component creation and unmount

So let's say there is acomponent which displays 2 child components: a document list and the selected document. By default the selected document component is not rendered, only when a document is selected from the list. And i also want this whole thing work when a new document is selected from the list.
There is a state which holds the document content and responsible for the selected document rendering, so i thought i'm going to set it to null in the method which handles the list item selection in order to unmount the previously created child component. Like this (excerpts from the parent class):
handleResultListItemClick(docname) {
if (this.state.sectioncontainer != null) this.setState({sectioncontainer: null},()=>{console.log("muhoo");});
var selected_doc = this.state.resultlist.filter((doc) => {
return docname === doc.properties.title;
});
this.setState({sectioncontainer: selected_doc[0].content.sections},()=>{console.log("boohoo");});
}
...
render() {
return (
...
{this.state.sectioncontainer != null && <SectionContainer listOfSections={this.state.sectioncontainer}/>}
);
}
The only problem is that state handling is not fast enough (or something) in react, because putting the state nullification and its new value setting in the same method results in no change in ReactDOM.
With the above code, the component will be created when the parent component first rendered, but after selecting a new doc in the list results in no change.
How should i implement this in way which works and also elegant?
I found this: ReactDOM.unmountComponentAtNode(container) in the official react docs. Is this the only way? If yes, how could i get this container 'name'?
Edit:
Based on the answers and thinking the problem a bit more through, i have to explain more of the context.
As kingdaro explained, i understand why there is no need to unmount a child component on a basic level, but maybe my problem is bit more sophisticated. So why did i want to unmount the child?
The documents consist of several subsections, hence the document object which is passed to the child component is an array of objects. And the document is generated dynamically based on this array the following way (excerpt from the SectionContainer class which is responsible to display the document):
buildSectionContainer() {
return this.props.listOfSections.map((section, index) =>
{
if (section.type === 'editor') return (
<QuillEditor
key={index}
id={section.id}
modules={modules}
defaultValue={section.content}
placeholder={section.placeholder}
/>
);
else if (section.type === 'text') return (
<div key={index}>{section.value}</div>
);
}
);
}
render() {
return (
<div>
{this.buildSectionContainer()}
</div>
);
}
The SectionContainer gets the array of objects and generate the document from it according to the type of these sections. The problem is that these sections are not updated when a different doc is selected in the parent component. I see change only when a bigger length array is passed to the child component. Like the firstly selected doc had an array of 2 elements, and then the newly selected doc had 3 elements array of sections and this third section is added to the previously existing 2, but the first 2 sections remained as they were.
And that’s why i though it’s better to unmount the child component and create a new one.
Surely it can happen that i miss something fundamental here again. Maybe related to how react handles lists. I just dont know what.
Edit2:
Ok, figured out that there is a problem with how i use the QuillEditor component. I just dont know what. :) The document updates, only the content of QuillEditors doesnt.
The reason your current solution doesn't actually do anything is because React's state updates are batched, such that, when setState is called a bunch of times in one go, React "combines" the result of all of them. It's not as much of a problem with being "not fast enough" as it is React performing only the work that is necessary.
// this...
this.setState({ message: 'hello', secret: 123 })
this.setState({ message: 'world' })
// ...becomes this
this.setState({ message: 'world', secret: 123 })
This behavior doesn't really have much to do with the problem at hand, though. As long as your UI is a direct translation of state -> view, the UI should simply update in accordance to the state.
class Example extends React.Component {
state = {
documentList: [], // assuming this comes from the server
document: null,
}
// consider making this function accept a document object instead,
// then you could leave out the .find(...) call
handleDocumentSelection = documentName => {
const document = this.state.documentList.find(doc => doc.name === documentName)
this.setState({ document })
}
render() {
const { document } = this.state
return (
<div>
<DocumentList
documents={this.state.documentList}
onDocumentSelection={this.handleDocumentSelection}
/>
{/*
consider having this component accept the entire document
to make it a little cleaner
*/}
{document && <DocumentViewer document={document.content.sections} />}
</div>
)
}
}

ReactJs trying to create a filter list

I am trying to create an filter list which filter the list on typed input. But don't know why the if statement is not working.
Here is the function I wrote: `
filterList (event) {
var updatedList = this.props.array;
var filterText = this.state.text;
updatedList = updatedList.filter(function(item){
console.log(filterText);
if(item.name === filterText){
console.log('check', item.name);
}
});
Can someone help me out on this here is a link to my codepen: LINK
Your filter needs to update the array in the component state to force a re-render. You don't update props to force a re-render, props are more for initial data, or consistent data, which can be updated from the top level.
Change the handle function to pass the text to the filterList function, and return the result
handleChange(event) {
var array = this.filterList(event.target.value);
this.setState({ text: event.target.value, array: array });
},
filterList (filterText) {
var updatedList = this.props.array;
return updatedList.filter(function(item){
return item.name === filterText;
});
}
Update getInitialState to use the props:
getInitialState() {
return { text:'', array: this.props.array};
}
Then use the state instead of props in your render:
var arrayComponents = this.state.array.map(function(photo) {
return <li className="photo photo-name">{photo.name} <img className="photo" src={photo.link}/></li>;
})
First, when I run your codepen example, I'm not seeing any errors indicating that this.state.text is undefined. It's not filtering anything, but it is displaying the value of this.state.text. So the issue is not quite what your question suggests it is...
So how to we get this thing filtering? Basically, the idea with ReactJS components is that everything related to the current state of your component should be in this.state, and any decisions about what to display based on that state should be in the render method. And keep in mind that any time you change the state using the setState method, it's going to trigger a re-rendering of your component.
With that in mind, I'd set things up like this:
your component receives a list of photos via the array prop (I'd probably call it something else, since it's not very descriptive and it's awfully close to a reserved word in Javascript)
the state for the component has one value: text (again, not very descriptive)
the component has a handleChange handler for the input element. I would connect this to the onChange handler for the input element - don't worry, it will be called every time the value in the input element changes. This should be the only event handler on the input element and all it should do is call this.setState({ text: event.target.value });
do the list filtering in the render method. The key here is that you don't need to keep a filtered list of your photos - all you're doing with it is rendering it, so do it only when you need to (or rather, when React thinks you need to and calls the render method). So your render method will look something like this:
render() {
var myFilteredList = this.props.array.filter(function(item){
console.log(filterText);
if(item.name === filterText){
console.log('check', item.name);
return true;
}
return false;
});
var arrayComponents = myFilteredList.map(function(photo) {
return <li className="photo photo-name">{photo.name} <img className="photo" src={photo.link}/></li>;
});
return (
<div>
<h1>Hello, {this.props.name}</h1>
<p>{this.state.text}</p>
<input type="text" onKeyUp={this.handleChange} />
<ul>
{arrayComponents}
</ul>
</div>
);
}
That's it. This has a couple of advantages:
it keeps it simple - don't maintain any state for the component outside of this.state and if you only need it for rendering, don't maintain it at all.
it keeps your code cleaner - the way I've approached it, your component has only two methods: handleChange and render
it (should be) more performant - React is pretty good at deciding when to render based on state changes, and it tends to be better when components have minimal state. (I say "should be" simply because I haven't tested it out at all).

The best way to retrieve data of a range of different child components in React Js

I am quite new to react js. Have searched a bit but haven't got an answer to this question:
Suppose I have two dynamic input tables in my page, and each of them is a separate component. And the lines are different components (React classes) as well. And on the page, there is a save button. Once the save button is clicked, all the information of the page should be gathered and pushed to server as a JSON string.
The very obvious approach for me is to gather the information via jQuery. This will definitely work. But it makes me feel it is not the react way of doing it. Since react data is one way binding, I am not quite sure how to handle this situation more appropriately. Any suggestions?
As always, there are a few ways of doing this.
1. Using refs.
You can assign references to input fields and then loop through them to get the values. The good thing about this approach is that no events are needed, but you still have to somehow know the reference to each. It adds complexity if you have dynamic fields or heavily nested fields. This is still my preferred way, mainly because you don't need events (e.g. keyup, blur, change, depending on your usage)
2. Using state
This makes it easier to instantly get values, if the values are updated in state as soon as the user makes a change to the field. Obviously you will need to know when a change has been made so you need events.
Your event callback can do one of many things, such as
update a global state object (e.g. via redux)
update form's values (or state) object via context usage
I hope this helps plan your forms.
Please check out this simplified example: https://jsfiddle.net/352v4n72/2/
It has three components. The most basic one is Input, which informs its parent when the value is changed:
var Input = React.createClass({
render: function() {
return (
<input type="text" placeholder={this.props.name}
onChange={e => this.props.onInputChange(e.currentTarget.value)} />
);
}
});
Now, its parent component, Line:
var Line = React.createClass({
onInputChange(inputId, value) {
var obj = {};
obj[inputId] = value;
this.setState(obj, state => {
this.props.onLineChange(this.props.lineId, this.state);
});
},
render: function() {
return (
<div>
<Input name="firstName" onInputChange={value => this.onInputChange('firstName', value)} />
<Input name="lastName" onInputChange={value => this.onInputChange('lastName', value)} />
<Input name="email" onInputChange={value => this.onInputChange('email', value)} />
</div>
);
}
});
This component holds three Input components. When each of them changes, the onInputChange function is called, and this function basically aggregates all the input values, creating a whole "line" data.
The last component is Table:
var Table = React.createClass({
onLineChange(lineId, value) {
var obj = {};
obj[lineId] = value;
this.setState(obj, state => console.log(this.state));
},
render: function() {
return (
<div>
<Line lineId={1} onLineChange={this.onLineChange} />
<Line lineId={2} onLineChange={this.onLineChange} />
<Line lineId={3} onLineChange={this.onLineChange} />
</div>
);
}
});
This component holds three lines, and just like the line aggregates Input's, Table aggregates Line's. You can see the state as it changes in the console.

Categories

Resources