I have a React App and I have an App container where I want to call a fetch to get some data to pass through to components that will need that information.
I've declared variables outside of the an onload function (where my fetch is located) and assigned them different values in the fetch. They variables will change in the fetch but outside of the fetch they remain unchanged.
How come they are not staying changed and is there a way to fix this?
I've tried changed the variables declared with var instead of let and I've tried putting the function inside the const.
I've also tried putting the fetch inside of other components (like Table as seen below) but then I have to declare two states and call a fetch withint a fetch because I'm already calling another fetch there and it becomes cumbersome...
let latestRelease = 0;
let draftRelease = 0;
let doClone = false;
function onload() {
fetch(url, {
method: 'GET'
})
.then(function(response) {
return response.json();
})
.then(function(result) {
for(var i = 0; i < result.length; i++)
{
if(result[i].id > latestRelease && result[i].status === "released") {
latestRelease = result[i].id;
}
if(result[i].id > draftRelease && result[i].status === "draft") {
draftRelease = result[i].id;
}
}
if(latestRelease > draftRelease) {
doClone = true;
}
})
.catch((error) => {
console.log(error);
});
}
const App: React.FC = () => {
onload()
return (
<React.Fragment>
<CssBaseline />
<Container fixed>
<PersistentDrawerLeft/>
{console.log(latestRelease)} //displays 0
<Title/>
<Table />
</Container>
</React.Fragment>
);
}
export default App;
I'm expecting for latestRelease and draftRelease to not stay as 0 but anything greater than that but the output is just 0. With the correct values returned I'd then like to pass them as props to the components.
Many thanks!
Part of the issue is that you don't seem to be properly distinguishing between synchronous and asynchronous code. fetch is asynchronous, meaning that that code is not guaranteed to run before anything else in the file. (fetch uses JS promises to manage async data, so it helps to have a good grasp on using promises.)
In a typical React case, you want to do a few things differently. First, you want to use component state to hold on to the data, rather than just random variables (this allows React to re-render when those values change). Secondly, when you're fetching data asynchronously, you need to work out what your app should do before the fetch is complete.
Here's a very basic example showing how this could work:
import React, { useState, useEffect } from 'react'
const App = ({ url }) => {
// We'll use this variable to store an object with the details
const [releaseDetails, setReleaseDetails] = useState(null)
// When the component is loaded, we'll fetch the url (coming from the component props) and then
// run your logic.
useEffect(() => {
let latestRelease = 0;
let draftRelease = 0;
let doClone = false;
fetch(url)
.then((response) => response.json())
.then((result) => {
for(var i = 0; i < result.length; i++) {
if(result[i].id > latestRelease && result[i].status === "released") {
latestRelease = result[i].id;
}
if(result[i].id > draftRelease && result[i].status === "draft") {
draftRelease = result[i].id;
}
}
if(latestRelease > draftRelease) {
doClone = true;
}
// To make these details available to the component, we'll bundle them into an object
// and update the component's state:
setReleaseDetails({
latestRelease,
draftRelease,
doClone
})
})
.catch((error) => {
// You'd ideally want some proper error handling here
console.log(error)
});
}, []) // pass an empty array so this is only run when the component is first rendered
// Because we're getting the data asynchronously, we need to display something while we wait
if(releaseDetails === null) {
return "loading..."
}
// Once the data is available, you can then use the details when rendering. You could instead
// render a child component and pass the values as props to it.
return (
`LatestRelease: ${releaseDetails.latestRelease}`
)
}
Speaking generally, there are probably a few React and general JS concepts you'll want to make sure you have your around, particularly around state and async data fetching. Not sure how much experience you've had with it so far, but you may want to take a look at some intro tutorials (possibly like this official one) to see how much you can follow and if there's anything that jumps out as something you need to familiarise yourself with more.
can you please try with state variable ,because if state variable changes the render will call again, here your using a normal variable may be its changing but its not rendering.
Thank You.
Variable should be in the state for re rendering
Related
I'm trying to make a page that gets picture from a server and once all pictures are downloaded display them, but for some reason the page doesn't re-render when I update the state.
I've seen the other answers to this question that you have to pass a fresh array to the setImages function and not an updated version of the previous array, I'm doing that but it still doesn't work.
(the interesting thing is that if I put a console.log in an useEffect it does log the text when the array is re-rendered, but the page does not show the updated information)
If anyone can help out would be greatly appreciated!
Here is my code.
export function Profile() {
const user = JSON.parse(window.localStorage.getItem("user"));
const [imgs, setImages] = useState([]);
const [num, setNum] = useState(0);
const [finish, setFinish] = useState(false);
const getImages = async () => {
if (finish) return;
let imgarr = [];
let temp = num;
let filename = "";
let local = false;
while(temp < num+30) {
fetch("/get-my-images?id=" + user.id + "&logged=" + user.loggonToken + "&num=" + temp)
.then(response => {
if(response.status !== 200) {
setFinish(true);
temp = num+30;
local = true;
}
filename = response.headers.get("File-Name");
return response.blob()
})
.then(function(imageBlob) {
if(local) return;
const imageObjectURL = URL.createObjectURL(imageBlob);
imgarr[temp - num] = <img name={filename} alt="shot" className="img" src={imageObjectURL} key={temp} />
temp++;
});
}
setNum(temp)
setImages(prev => [...prev, ...imgarr]);
}
async function handleClick() {
await getImages();
}
return (
<div>
<div className="img-container">
{imgs.map(i => {
return (
i.props.name && <div className="img-card">
<div className="img-tag-container" onClick={(e) => handleView(i.props.name)}>{i}</div>
<div className="img-info">
<h3 className="title" onClick={() => handleView(i.props.name)}>{i.props.name.substr(i.props.name.lastIndexOf("\\")+1)}<span>{i.props.isFlagged ? "Flagged" : ""}</span></h3>
</div>
</div>
)
})}
</div>
<div className="btn-container"><button className="load-btn" disabled={finish} onClick={handleClick}>{imgs.length === 0 ? "Load Images" : "Load More"}</button></div>
</div>
)
}
I think your method of creating the new array is correct. You are passing an updater callback to the useState() updater function which returns a concatenation of the previous images and the new images, which should return a fresh array.
When using collection-based state variables, I highly recommend setting the key property of rendered children. Have you tried assigning a unique key to <div className="img-card">?. It appears that i.props.name is unique enough to work as a key.
Keys are how React associates individual items in a collection to their corresponding rendered DOM elements. They are especially important if you modify that collection. Whenever there's an issue with rendering collections, I always make sure the keys are valid and unique. Even if adding a key doesn't fix your issue, I would still highly recommend keeping it for performance reasons.
It is related to Array characteristics of javascript.
And the reason of the console log is related with console log print moment.
So it should be shown later updated for you.
There are several approaches.
const getImages = async () => {
... ...
setNum(temp)
const newImage = [...prev, ...imgarr];
setImages(prev => newImage);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => JOSN.parse(JSON.object([...prev, ...imgarr]);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => [...prev, ...imgarr].slice(0));
}
Maybe it could work.
Hope it will be helpful for you.
Ok the problem for me was the server was not sending a proper filename header so it was always null so the condition i.props.name was never true... lol sorry for the confusion.
So the moral of this story is, always make sure that it's not something else in your code that causes the bad behavior before starting to look for other solutions...
So I'm currently trying to learn react and as practice I was just trying to build a hacker news site using the hacker new API. But, I ran into a issue. For some reason it is currently infinitely looping. I debugged it and found that it has something to do with the useEffect() hook. I tried the solution in this post but it didn't seam to work(I think I probably did it wrong though).
My Code:
const [maindata, setmaindata] = useState("");
useEffect(() => {
axios
.get("https://hacker-news.firebaseio.com/v0/user/jl.json?print=pretty")
.then((repo) => {
const output = [];
// repo.data["submitted"].length
for (let x = 0; x < 30; x++) {
axios
.get(
"https://hacker-news.firebaseio.com/v0/item/" +
repo.data["submitted"][x] +
".json?print=pretty"
)
.then((titledata) => {
//console.log(titledata.data["text"]);
output.push(titledata.data["text"]);
});
}
});
setmaindata(output);
});
I also tried replacing:
}
});
setmaindata(output);
});
With:
}
});
}, [output});
But that didn't seem to work
If you don't pass the dependency array to useEffect, useEffect runs on every render.
By pass empty array([]) as dependency, useEffect runs once when component is mounted.
Ex:
useEffect(() => {
... // your code
}, [])
You should add dependency to useEffect because if you don't add any dependency, this method infinitely runs.
Just implement at the end of method [].
In addition, take care with setMainData because you call it outside of axis request.
Final code could be this:
const [maindata, setmaindata] = useState("");
useEffect(() => {
axios
.get("https://hacker-news.firebaseio.com/v0/user/jl.json?print=pretty")
.then((repo) => {
const output = [];
// repo.data["submitted"].length
for (let x = 0; x < 30; x++) {
axios
.get(
"https://hacker-news.firebaseio.com/v0/item/" +
repo.data["submitted"][x] +
".json?print=pretty"
)
.then((titledata) => {
//console.log(titledata.data["text"]);
output.push(titledata.data["text"]);
// here you have output array with the push action that you did in previous line
setmaindata(output);
});
}
});
}, [dependency]);
Change dependency with your variable that you want to use when this value changes this useEffect will be called
The question: why doesn't react display my images in the JSX? How can I get it to display the images?
Edit: the issue I suspect is not with the way I get the data but rather the way I try to display/render the image, which I have labeled under main issue below.
I have a functional component that has a useEffect to GET a JSON object from firebase.
useEffect(()=>{
axios
.get("http://localhost:9998/api/v1/fault/" + lastURLSegment)
.then((response) => {
setDisplay(response.data)
})
},[])
Then I extract the imageURL value and query firebase to return me a url viewable on the web
e.g https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media
once I am able to get the URL for the image, I setState into an array called objUrl
edit: there is objOfUrl and objUrl, objOfUrl is a var [], objOfUrl is a state
useEffect(()=>{
// displayspecificCases.imgURL = "fault/asd.jpg"
let foo = displayspecificCases.imageurl // "fault/asdfsdf.jpg,fault/1234.jpg..."
if (foo !== undefined){
console.log(foo) //console logs 2 images in string saperated by comma
let bar = foo.split(','); // bar is now an array ['fault/asdfsdf.jpg','fault/1234.jpg']
let i = 0;
bar.map((item)=>{
console.log(item)
if(item !== ""){
firebase.storage().ref()
.child(item)
.getMetadata()
.then((results) => {
objOfUrl[i] = `https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media`
i++ // i ++ for objOfUrl
console.log("i is :" + i)
try{setObjUrl(objOfUrl)}catch(e){console.log("err is in view useEffect try catch e : " + e)}
})
.catch((err) => {
console.log(err);
});
console.log(objOfUrl); //console logs [0: "url.....", 1: "url....."]
}
})
}
},[displayspecificCases])
Main issue is here!
So I want to display the images in JSX. So I setState the array into a state and tried to use .map to return each item in the array so I can use img tag to display them
// inside the JSX
{objUrl.map((item, i) => { console.log(i); return <div class="column"> <img key={i} width="360px" height="270px" src={item} alt="no photo"/> </div>})}
But the problem right now is that only 1 image is displayed and not the whole array. Also whenever it renders, it seems to randomly display one of images inside the array. How do I display all images inside the array? many thanks!
sidenote: I know my code is messy, I'm still learning, please do give advice and tips on how to improve certain parts!
If I understand your code correctly it is doing:
Effect callback triggered by displayspecificCases updating
Split displayspecificCases.imageurl into array of urls
Call firebase backend for the bucket the image is in for the real image URL
Issues
You are mutating your objOfUrl state object.
objOfUrl[i] = `https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media`
You enqueue standard state updates in a loop. This enqueues all state updates using the state value from the previous render cycle (remember state is const). This is also a problem since objOfUrl IS the state reference. It should be a new array reference.
setObjUrl(objOfUrl)
I suppose this isn't a cause for your issue, but it is incorrect to use array.prototype.map to issue array side-effects.
Solution
Use a functional state update to
Correctly enqueue state updates in a loop
Correctly return a new state array reference
Code
useEffect(() => {
if (displayspecificCases.imageurl) {
displayspecificCases.imageurl.split(",").forEach((item, i) => {
console.log(item);
if (item !== "") {
firebase
.storage()
.ref()
.child(item)
.getMetadata()
.then((results) => {
// Construct complete image URL
const url = `https://firebasestorage.googleapis.com/v0/b/${
results.bucket
}/o/${encodeURIComponent(item)}?alt=media`;
console.log("i is :" + i);
try {
// Update state array at index i
setObjUrl((objOfUrl) =>
objOfUrl.map((objUrl, index) => (index === i ? url : objUrl))
);
} catch (e) {
console.log("err is in view useEffect try catch e : " + e);
}
})
.catch((err) => {
console.log(err);
});
}
});
}
}, [displayspecificCases]);
// Log state when it updates
useEffect(() => {
console.log(objOfUrl); //console logs [0: "url.....", 1: "url....."]
}, [objOfUrl]);
Solved
solution explained below
useEffect(()=>{
axios
.get("http://localhost:9998/api/v1/fault/" + lastURLSegment)
.then(async (response) => {
setDisplay(response.data)
const imagesToConstruct = await response.data.imageurl.slice(0,-1).split(",")
// console.log(imagesToConstruct)
imagesToConstruct.length > 0 && imagesToConstruct.forEach((item, i) => {
if (item !== "") {
firebase
.storage()
.ref()
.child(item)
.getMetadata()
.then((results) => { //main solution here
setImages((prevImages) => [...prevImages, `https://firebasestorage.googleapis.com/v0/b/${results.bucket}/o/${encodeURIComponent(item)}?alt=media`])
}).catch((err) => {
console.log(err);
});
}
})
})
}, [])
The issue
Every time useEffect is called, the constructed url is parsed into a temp array (objOfUrl) which is setState into objUrl by using setobjUrl(objOfUrl) However, for some reason, i cannot figure out why it overwrites the state and the state only ends up with one constructed URL. So i had to consult someone to help with my code and this was the solution he provided.
The solution explained
Compressed the 2 useEffect into 1 and then set new state (images) using the spread operator.
Thank you to those who have attempted to help and replied to this post!
I have the following react code, that represents a text input.
onChangeDestination(url, index) {
this.setState(prevState => {
const rules = [...prevState.rules];
rules[index] = { ...rules[index], url};
if (isURL(url+'')) {
testURL(url).then(r=> {
var url_status = r.data.response
rules[index] = { ...rules[index], url_status};
})
} else {
var url_status = "No URL"
rules[index] = { ...rules[index], url_status};
}
return { rules };
});
};
In English:
If the URL passes isURL() validation, then use custom function testURL() to see what the HTTP status of that URL is (using axios).
In my template, there's a {props.url_status} for the relevant bit.
The issue is, even though it's logging to the console the desired behaviour, it doesn't seem to be updating the viewport reliably, which I think is linked to the promise.
What am I doing wrong?
You could achieve it by converting your function to be asynchronous and calling your promise (if necessary) before your setState. This solution uses the easier to read async/await syntax and a ternary condition to choose the correct status value :
const url_status = isURL(url + '') ? (await testURL(url)).data.response : "No URL"
This line will execute your promise and wait for it only if isURL return true, if so it will return the response part and if not, it will send out "No URL".
Full code :
async onChangeDestination(url, index) {
const url_status = isURL(url + '') ? (await testURL(url)).data.response : "No URL"
this.setState(prevState => {
const rules = [...prevState.rules];
rules[index] = {
...rules[index],
url,
url_status
};
return { rules };
});
};
I recommend to use more components with some particular single task only. In this case: you might need a stateless Input component with an onChange and a value prop. Its parent could be some container where the onChange triggers some async request and has a state like url_status. As #Nit commented set the state in the promise then clause. This url_status will be a prop of some other component, so in case of the prop changes that component will re-render automatically... In most of the cases you do not need to use states at all.
So I just started trying to learn rxjs and decided that I would implement it on a UI that I'm currently working on with React (I have time to do so, so I went for it). However, I'm still having a hard time wrapping my head around how it actually works... Not only "basic" stuff like when to actually use a Subject and when to use an Observable, or when to just use React's local state instead, but also how to chain methods and so on. That's all too broad though, so here's the specific problem I have.
Say I have a UI where there's a list of filters (buttons) that are all clickeable. Any time I click on one of them I want to, first of all, make sure that the actions that follow will debounce (as to avoid making network requests too soon and too often), then I want to make sure that if it's clicked (active), it will get pushed into an array and if it gets clicked again, it will leave the array. Now, this array should ultimately include all of the buttons (filters) that are currently clicked or selected.
Then, when the debounce time is done, I want to be able to use that array and send it via Ajax to my server and do some stuff with it.
import React, { Component } from 'react';
import * as Rx from 'rx';
export default class CategoryFilter extends Component {
constructor(props) {
super(props);
this.state = {
arr: []
}
this.click = new Rx.Subject();
this.click
.debounce(1000)
// .do(x => this.setState({
// arr: this.state.arr.push(x)
// }))
.subscribe(
click => this.search(click),
e => console.log(`error ---> ${e}`),
() => console.log('completed')
);
}
search(id) {
console.log('search --> ', id);
// this.props.onSearch({ search });
}
clickHandler(e) {
this.click.onNext(e.target.dataset.id);
}
render() {
return (
<section>
<ul>
{this.props.categoriesChildren.map(category => {
return (
<li
key={category._id}
data-id={category._id}
onClick={this.clickHandler.bind(this)}
>
{category.nombre}
</li>
);
})}
</ul>
</section>
);
}
}
I could easily go about this without RxJS and just check the array myself and use a small debounce and what not, but I chose to go this way because I actually want to try to understand it and then be able to use it on bigger scenarios. However, I must admit I'm way lost about the best approach. There are so many methods and different things involved with this (both the pattern and the library) and I'm just kind of stuck here.
Anyways, any and all help (as well as general comments about how to improve this code) are welcome. Thanks in advance!
---------------------------------UPDATE---------------------------------
I have implemented a part of Mark's suggestion into my code, but this still presents two problems:
1- I'm still not sure as to how to filter the results so that the array will only hold IDs for the buttons that are clicked (and active). So, in other words, these would be the actions:
Click a button once -> have its ID go into array
Click same button again (it could be immediately after the first
click or at any other time) -> remove its ID from array.
This has to work in order to actually send the array with the correct filters via ajax. Now, I'm not even sure that this is a possible operation with RxJS, but one can dream... (Also, I'm willing to bet that it is).
2- Perhaps this is an even bigger issue: how can I actually maintain this array while I'm on this view. I'm guessing I could use React's local state for this, just don't know how to do it with RxJS. Because as it currently is, the buffer returns only the button/s that has/have been clicked before the debounce time is over, which means that it "creates" a new array each time. This is clearly not the right behavior. It should always point to an existing array and filter and work with it.
Here's the current code:
import React, { Component } from 'react';
import * as Rx from 'rx';
export default class CategoryFilter extends Component {
constructor(props) {
super(props);
this.state = {
arr: []
}
this.click = new Rx.Subject();
this.click
.buffer(this.click.debounce(2000))
.subscribe(
click => console.log('click', click),
e => console.log(`error ---> ${e}`),
() => console.log('completed')
);
}
search(id) {
console.log('search --> ', id);
// this.props.onSearch({ search });
}
clickHandler(e) {
this.click.onNext(e.target.dataset.id);
}
render() {
return (
<section>
<ul>
{this.props.categoriesChildren.map(category => {
return (
<li
key={category._id}
data-id={category._id}
onClick={this.clickHandler.bind(this)}
>
{category.nombre}
</li>
);
})}
</ul>
</section>
);
}
}
Thanks, all, again!
Make your filter items an Observable streams of click events using Rx.Observable.fromevent (see https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/events.md#converting-a-dom-event-to-a-rxjs-observable-sequence) - it understands a multi-element selector for the click handling.
You want to keep receiving click events until a debounce has been hit (user has enabled/disabled all filters she wants to use). You can use the Buffer operator for this with a closingSelector which needs to emit a value when to close the buffer and emit the buffered values.
But leaves the issue how to know the current actual state.
UPDATE
It seems to be far easier to use the .scan operator to create your filterState array and debounce these.
const sources = document.querySelectorAll('input[type=checkbox]');
const clicksStream = Rx.Observable.fromEvent(sources, 'click')
.map(evt => ({
name: evt.target.name,
enabled: evt.target.checked
}));
const filterStatesStream = clicksStream.scan((acc, curr) => {
acc[curr.name] = curr.enabled;
return acc
}, {})
.debounce(5 * 1000)
filterStatesStream.subscribe(currentFilterState => console.log('time to do something with the current filter state: ', currentFilterState);
(https://jsfiddle.net/crunchie84/n1x06016/6/)
Actually, your problem is about RxJS, not React itself. So it is easy. Suppose you have two function:
const removeTag = tagName =>
tags => {
const index = tags.indexOf(index)
if (index !== -1)
return tags
else
return tags.splice(index, 1, 0)
}
const addTag = tagName =>
tags => {
const index = tags.indexOf(index)
if (index !== -1)
return tags.push(tagName)
else
return tags
}
Then you can either using scan:
const modifyTags$ = new Subject()
modifyTags$.pipe(
scan((tags, action) => action(tags), [])
).subscribe(tags => sendRequest(tags))
modifyTags$.next(addTag('a'))
modifyTags$.next(addTag('b'))
modifyTags$.next(removeTag('a'))
Or having a separate object for tags:
const tags$ = new BehaviorSubject([])
const modifyTags$ = new Subject()
tags$.pipe(
switchMap(
tags => modifyTags$.pipe(
map(action => action(tags))
)
)
).subscribe(tags$)
tags$.subscribe(tags => sendRequest(tags))