how to create refs for content that gets created later - reactjs

I have a component that fetches data from an api upon user input. This data then gets rendered onto the screen as <li/> tags. I want those <li/> tags to have a ref.
I tried creating an object of refs that I create after the data is fetched:
this.singleRefs = data.reduce((acc, value) => {
acc[value.id] = React.createRef();
return acc;
}, {});
and then later assign these refs to the <li/> tag: <li ref={this.singleRefs[element.id]}>
but when I print them out I always have {current:null} Here is a demo
what am I doing wrong?

With dynamic ref data, I'd propose that you should use callback refs.
import React from "react";
import "./styles.css";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
this.singleRefs = {};
}
componentDidMount() {
const data = [
{ value: "val1", id: 1 },
{ value: "val2", id: 2 },
{ value: "val3", id: 3 }
];
this.myFunc(data);
//you don't need this anymore
// this.singleRefs = data.reduce((acc, value) => {
// acc[value.id] = React.createRef();
// return acc;
// }, {});
}
myFunc = async (data) => {
await sleep(3000);
this.setState({ data });
};
renderContent() {
return this.state.data.map(
function (element, index) {
return (
<li key={index} ref={(node) => (this.singleRefs[element.id] = node)}>
{element.value}
</li>
);
}.bind(this)
);
}
render() {
console.log(this.singleRefs);
return <ul>{this.renderContent()}</ul>;
}
}
Sandbox

Related

Firebase Firestore data is not showing in my map function for React

I am trying to fill my state array with my document data from my database. I am getting no errors, however I am not getting any of my data to display. In my firestore database I have a collection "messages" and then two auto ID document with a title and message as fields in each. The data is not showing at all.
export class WindowUplifting extends Component {
constructor(props) {
super(props);
this.state = {
arra: null,
items: [],
};
this.state.messagesEndRef = React.createRef();
}
async componentDidMount() {
//this.setState({ groupId: this.props.currentGroup.id });
this.scrollToBottom();
// this.getListner();
let itemArr = [];
db.collection("messages")
.get()
.then((querySnapshot) => {
//Notice the arrow funtion which bind `this` automatically.
querySnapshot.forEach(function (doc) {
itemArr.push(doc.data());
});
this.setState({ items: itemArr }); //set data in state here
});
}
componentDidUpdate() {
this.scrollToBottom();
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentGroup) {
this.setState({ groupId: nextProps.currentGroup.id });
}
}
componentWillUnmount() {
if (this.state.unsubscribe != null) this.state.unsubscribe(); //remove listener
}
scrollToBottom = () => {
this.state.messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
};
render() {
const { classes } = this.props;
const items = this.state.items;
return (
<div className={classes.welcome}>
<ul className={classes.chatList}>
{items.length >= 0 &&
items.map((item) => (
<li>
<h1> {item.title}</h1>
<Divider style={{ marginBottom: 10, marginTop: 10 }} />
<p>{item.body}</p>
</li>
))}
......

React Native : Dynamically Created TextBox value is not getting updated

I'm little new to React Native. I have a scenario where I need to create the TextInput dynamically and bind values it from an array. Once the array updates, the value of the TextInput is not updating. Below is my code.
constructor(props) {
super(props);
this.state = {
textInputValues: [],
textInput: [],
samplearray://gets an array from the JSON
}
componentDidMount() {
this.setTextInputValue();
this.prepareTextBox();
}
setTextInputValue() {
let textInputValues = this.state.textInputValues;
this.state.samplearray.map(() => {
textInputValues.push("") //default value
this.setState({ textInputValues })
})
}
prepareTextBox() {
let textInput = this.state.textInput;
this.state.samplearray.map((value, index) => {
textInput.push(<TextInput style={styles.textBox} value={this.state.textInputValues[index]} key={index} />);
})
this.setState({ textInput })}
Code to render the TextBox in the render method.
{ this.state.textInput.map((value, index) => {
return value
})}
I have button on which this.state.textInputValues array value gets changed. But change of that is not being reflected in the TextInput. Stuck with this since 2 days. Any help is appreciated, thanks in advance.
This is how your code block should look (do read the comments for explanation):
componentDidMount() {
this.setTextInputValue();
// call the below function from `setTextInputValue` as you have dependency on that
// this.prepareTextBox();
}
setTextInputValue() {
let textInputValues = [...this.state.textInputValues];
this.state.samplearray.map((value) => {
textInputValues = [ ...textInputValues , value] //default value
// this is how you should call `prepareTextBox`
// in setState callback as it will confirm that state is updated
this.setState({ textInputValues },() => {
this.prepareTextBox();
})
})
}
prepareTextBox() {
let textInput = [...this.state.textInput];
this.state.samplearray.map((value, index) => {
textInput.push(<input value={this.state.textInputValues[index]} key={index} />);
})
this.setState({ textInput })
}
You can run the below snippet and check, hope that will clear your doubts :
const { useState , useEffect } = React;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
textInputValues: [],
textInput: [],
samplearray:["Vivek","Darshita"]//gets an array from the JSON
}
}
componentDidMount() {
this.setTextInputValue();
}
setTextInputValue() {
let textInputValues = [...this.state.textInputValues];
this.state.samplearray.map((value) => {
textInputValues = [ ...textInputValues , value] //default value
this.setState({ textInputValues },() => {
this.prepareTextBox();
})
})
}
prepareTextBox() {
let textInput = [...this.state.textInput];
this.state.samplearray.map((value, index) => {
textInput.push(<input value={this.state.textInputValues[index]} key={index} />);
})
this.setState({ textInput })
}
changeValues = () => {
this.setState({
textInput : [],
textInputValues : ["New - Vivek" , "New - Darshita"]
},() => {
this.prepareTextBox();
});
}
render() {
return (
<div>
{ this.state.textInput }
<button onClick={this.changeValues}>Change Value</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('react-root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react-root"></div>

I think render works twice

I'm only learning React, trying to write a simple TODO list app. When I'm trying to add a new task, two identical tasks are added. I tried to debug by the console.log element and saw a problem. render works twice, so my button sends info to the function twice. Can someone please guide me to the solution? Here is the code.
import React from 'react';
class TaskInput extends React.Component {
constructor(props) {
super(props);
this.state = {
input: ''
};
}
addTask = () => {
const { input } = this.state;
if (input) {
this.props.addTask(input);
this.setState({ input: '' });
}
};
handleEnter = event => {
if (event.key === 'Enter') this.addTask();
};
inputChange = event => {
this.setState({ input: event.target.value });
};
render() {
const { input } = this.state;
console.log(this.state);
return (
<div className="task-input">
<input
type="text"
onKeyPress={this.handleEnter}
onChange={this.inputChange}
value={input}
></input>
<button onClick={this.addTask } >ADD</button>
</div>
);
}
}
export default TaskInput;
Here is the App.js code:
import React from 'react';
import Task from './components/Task';
import TaskInput from './components/TaskInput';
class App extends React.Component {
constructor () {
super();
this.state = {
tasks: [
{id: 0, title: 'Create Todo-app', done: false},
{id: 1, title: 'Do smth else', done: true},
{id: 2, title: 'Do more things', done: false}
]
};
}
addTask = task => {
this.setState(state => {
let {tasks} = state;
console.log("state");
tasks.push({
id: tasks.length !==0 ? tasks.length : 0,
title: task,
done: false
});
return tasks;
});
}
doneTask = id => {
const index = this.state.tasks.map(task => task.id).indexOf(id);
this.setState(state => {
let {tasks} = state;
tasks[index].done = true;
return tasks;
});
};
deleteTask = id => {
const index = this.state.tasks.map(task => task.id).indexOf(id);
this.setState(state => {
let {tasks} = state;
delete tasks[index];
return tasks;
})
};
render() {
const { tasks } = this.state;
const activeTasks = tasks.filter(task => !task.done);
const doneTasks = tasks.filter(task => task.done)
return (
<div className = "App">
<h1 className="top">Active tasks: {activeTasks.length}</h1>
{[...activeTasks, ...doneTasks].map(task => (
<Task
doneTask={() => this.doneTask(task.id)}
deleteTask={() => this.deleteTask(task.id)}
task={task}
key={task.id}
></Task>))}
<TaskInput addTask={this.addTask}></TaskInput>
</div>
);
}
}
export default App;
I think you are accidentally directly modifying the state inside addTask.
The line let {tasks} = state; is creating a reference to the original state, rather than a new copy, and then your push modifies the state directly.
Using expansion/spread syntax to get a copy of your array like this should work:
addTask = task => {
this.setState(state => {
const tasks = [ ...state.tasks ];
tasks.push({
id: tasks.length !==0 ? tasks.length : 0,
title: task,
done: false
});
return { tasks };
});
}
Using let tasks = [ ...state.tasks ]; will create a new array rather than a reference, and prevent the state from being modified directly.
The reason you were seeing double results was that you effectively set the state with the push, and then set it again with the returned value.
I've changed your code a little bit. It's working here. Would you please check?
class TaskInput extends React.Component {
constructor(props) {
super(props);
this.state = {
input: "",
tasks: []
};
}
addTask = newTask => {
this.setState(state => ({
...state,
input: "",
tasks: [...state.tasks, newTask]
}));
};
handleEnter = event => {
if (event.key === "Enter") this.addTask(event.target.value);
};
inputChange = event => {
this.setState({ input: event.target.value });
};
render() {
const { input } = this.state;
console.log(this.state);
return (
<div className="task-input">
<input
onKeyPress={this.handleEnter}
onChange={this.inputChange}
value={input}
></input>
<button onClick={this.addTask}>ADD</button>
</div>
);
}
}
ReactDOM.render(<TaskInput/>, document.querySelector("#root"));
.as-console-wrapper {
max-height: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

React App Rendering Before Firestore Data Has Loaded

I am trying to load data from Firestore and show it in the gantt-chart, but it renders before it has loaded the data from firebase. So I call setState inside of componentDidMount because I thought this would then call the render again at which point the data would be there. But it is still sitting empty. Any ideas as to why?
import React, { Component } from 'react';
import Gantt from './Gantt';
import Toolbar from './Toolbar';
import MessageArea from './MessageArea';
import Firebase from './Firebase';
import './App.css';
class App extends Component {
constructor(props) {
super(props);
this.state = {
currentZoom: 'Days',
messages: [],
projects: [],
links: []
};
this.handleZoomChange = this.handleZoomChange.bind(this);
this.logTaskUpdate = this.logTaskUpdate.bind(this);
this.logLinkUpdate = this.logLinkUpdate.bind(this);
}
componentDidMount() {
const db = Firebase.firestore();
var projectsArr = [];
db.collection('projects').get().then((snapshot) => {
snapshot.docs.forEach(doc => {
let project = doc.data();
projectsArr.push({id: 1, text: project.name, start_date: '15-04-2017', duration: 3, progress: 0.6});
});
});
this.setState({
projects: projectsArr
});
}
addMessage(message) {
var messages = this.state.messages.slice();
var prevKey = messages.length ? messages[0].key: 0;
messages.unshift({key: prevKey + 1, message});
if(messages.length > 40){
messages.pop();
}
this.setState({messages});
}
logTaskUpdate(id, mode, task) {
let text = task && task.text ? ` (${task.text})`: '';
let message = `Task ${mode}: ${id} ${text}`;
this.addMessage(message);
}
logLinkUpdate(id, mode, link) {
let message = `Link ${mode}: ${id}`;
if (link) {
message += ` ( source: ${link.source}, target: ${link.target} )`;
}
this.addMessage(message)
}
handleZoomChange(zoom) {
this.setState({
currentZoom: zoom
});
}
render() {
var projectData = {data: this.state.projects, links: this.state.links};
return (
<div>
<Toolbar
zoom={this.state.currentZoom}
onZoomChange={this.handleZoomChange}
/>
<div className="gantt-container">
<Gantt
tasks={projectData}
zoom={this.state.currentZoom}
onTaskUpdated={this.logTaskUpdate}
onLinkUpdated={this.logLinkUpdate}
/>
</div>
<MessageArea
messages={this.state.messages}
/>
</div>
);
}
}
export default App;
You are calling setState outside of the then callback.
So Change
db.collection('projects').get().then((snapshot) => {
snapshot.docs.forEach(doc => {
let project = doc.data();
projectsArr.push({id: 1, text: project.name, start_date: '15-04-2017', duration: 3, progress: 0.6});
});
});
this.setState({
projects: projectsArr
});
To
db.collection('projects').get().then((snapshot) => {
snapshot.docs.forEach(doc => {
let project = doc.data();
projectsArr.push({id: 1, text: project.name, start_date: '15-04-2017', duration: 3, progress: 0.6});
});
this.setState({
projects: projectsArr
});
});
Also, as a general pattern you can do something like this:
class AsyncLoad extends React.Component {
state = { data: null }
componentDidMount () {
setTimeout(() => {
this.setState({ data: [1, 2, 3]})
}, 3000)
}
render () {
const { data } = this.state
if (!data) { return <div>Loading...</div> }
return (
<pre>{JSON.stringify(data, null, 4)}</pre>
)
}
}
It's a common enough operation to create an HOC for it.

Reactjs - Assigning json response to default array

I'm using react-image-gallery for displaying gallery. I need to load images from json response. My code follows,
let imagesArray = [
{
original: 'images/products/4.jpg'
},
{
original: 'images/products/2.jpg'
},
{
original: 'images/products/3.jpg'
}
];
export default class Products extends React.Component {
loadGallery () {
var requestUrl = 'http://myurl';
var myInit = { method: 'GET',
mode: 'cors',
cache: 'default' };
fetch(requestUrl).then(response =>
response.json().then(data => ({
data: data,
status: response.status
})
).then(res => {
let imagesArray = imagesArray.map((img,i)=>{ return {original: res.data[i].path.split(':')[1]}})
}));
}
render() {
return (
<div className="products-page" onLoad={ this.loadGallery() }>
<ImageGallery
ref={i => this._imageGallery = i}
items={imagesArray}/>
</div>
);
}
}
I got an error of Uncaught (in promise) TypeError: Cannot read property 'map' of undefined
if I use let newArray = imagesArray.map((img,i)=>{ return {original: res.data[i].path.split(':')[1]}}) it will assign the value to newArray
Here how can I assign the json response to imagesArray?
If what I understand is correct you want to load a set of images and pass the data as array to ImageGallery component.
There is also something wrong/undesired with your code:
When you do this,
<div className="products-page" onLoad={this.loadGallery()}>
You will actually invoke the function loadGallery() on each render, instead you should only pass the function prototype.
<div className="products-page" onLoad={this.loadGallery}>
Below you will see another approach to implement your requirement. Here we will load the images and update Product component's state with new imagesArray from JSON response. So when the state updates, component re-renders and will pass the new data to ImageGallery component.
const dummyImagesArray = [
{
original: 'images/products/4.jpg'
},
{
original: 'images/products/2.jpg'
},
{
original: 'images/products/3.jpg'
}
];
export default class Products extends React.Component {
constructor(props) {
super(props);
this.state = {
imagesArray: dummyImagesArray
}
}
loadGallery () {
var requestUrl = 'http://myurl';
var myInit = { method: 'GET',
mode: 'cors',
cache: 'default' };
fetch(requestUrl).then(response =>
response.json().then(data => ({
data: data,
status: response.status
})
).then(res => {
const imagesArray = this.state.imagesArray.map((img,i) => { return { original: res.data[i].path.split(':')[1] }; });
this.setState({ imagesArray });
}));
}
render() {
return (
<div className="products-page" onLoad={this.loadGallery}>
<ImageGallery
ref={i => this._imageGallery = i}
items={this.state.imagesArray}
/>
</div>
);
}
}
So JSON does not actually exist in React. What you will likely need to do here is set up a class that represents the JSON data and returns it to the map. What map is basically doing is peeling back the layers of your object one by one. So something like,
class ImagesArray extends React.Component() {
render() {
return {
<image src={this.props.original}/>
}
}
}

Resources