I am trying to retrieve props from an internal api for a component, PresetFetcher, and pass them to a child component, PromptForm. My console logs show that the data is pulled from the API correctly and saved into an object {preset} which has the correct data. But no matter what I do, when I try to pass {preset} to the child component, it reports null value and fails to compile. What is wrong here? It must be some very basic misunderstanding on my part.
To keep it simple, I am avoiding state or effect, since the data does not need to be refreshed once the PromptForm receives it.
import axios from 'axios'
import { useParams } from 'react-router-dom'
import React, { useEffect,useState } from 'react'
import PromptForm from './PromptForm';
const API_SEARCH_BASE = 'http://0.0.0.0:5001/api2/preset_loader';
function PresetFetcher(props) {
const { preset_name } = useParams();
const API_SEARCH_URL = API_SEARCH_BASE + '/' + preset_name;
console.log('API_SEARCH_URL: ' + API_SEARCH_URL);
console.log('props entering PresetFetcher page', props);
const test = [{ name: 'item 1' }, { name: 'item2' }];
axios.get(API_SEARCH_URL)
.then
(response => {
console.log('PresetFetcher: response: ', response.data);
const preset = response.data;
console.log('preset after const', preset);
var preset_description = preset[0].preset_description;
console.log('preset_description: ', preset_description);
})
return (
<div> <PromptForm preset_out = {preset_description} /></div>
)
{console.log('props leaving Presetfetcher: ', props)};
}
export default PresetFetcher;
[HMR] Waiting for update signal from WDS...
index.js:8 Array(1)
launcher.js:11 on LauncherPage, preset name is baby-name-generator
launcher.js:12 props on launcherpage are Object
PresetFetcher.js:13 API_SEARCH_URL: http://0.0.0.0:5001/api2/preset_loader/baby-name-generator
PresetFetcher.js:14 props entering PresetFetcher page Object
PresetFetcher.js:13 API_SEARCH_URL: http://0.0.0.0:5001/api2/preset_loader/baby-name-generator
PresetFetcher.js:14 props entering PresetFetcher page Object
PresetFetcher.js:21 PresetFetcher: response: Array(1)
PresetFetcher.js:23 preset after const Array(1)
PresetFetcher.js:25 preset_description: Simple baby name generator powered by OpenAI's GPT-3 model gives you creative alternatives from a fresh perspective.
PresetFetcher.js:21 PresetFetcher: response: Array(1)
PresetFetcher.js:23 preset after const Array(1)0: {id: "2", preset_name: "Baby Name Generator (by Attributes)", preset_author: "WebBabyShower.com", preset_active: "True", preset_launched: "20210610", …}length: 1[[Prototype]]: Array(0)
PresetFetcher.js:25 preset_description: Simple baby name generator powered by OpenAI's GPT-3 model gives you creative alternatives from a fresh perspective.
ReferenceError: preset_description is not defined
PresetFetcher
src/components/PresetFetcher.js:28
25 | console.log('preset_description: ', preset_description);
26 | })
27 | return (
> 28 | <div> <PromptForm preset_out = {preset_description} /></div>
| ^ 29 |
30 | )
31 | {console.log('props leaving Presetfetcher: ', props)};
the API data looks like this. there is only one item in the list.
[
{
"id": "2",
"preset_name": "Baby Name Generator (by Attributes)",
"preset_author": "WebBabyShower.com",
"preset_active": "True",
"preset_launched": "20210610",
"preset_description": "Simple baby name generator powered by OpenAI's GPT-3 model gives you creative alternatives from a fresh perspective.",
"preset_instructions":
That variable is not available in the scope you're calling it. Try moving it outside the then block. I suggest storing it in local state so the component updates when the fetch completes.
const [presetDescription, setPresetDescription] = useState('')
axios.get(API_SEARCH_URL)
.then
(response => {
const preset = response.data;
setPresetDescription(preset[0].preset_description);
})
<PromptForm preset_out={presetDescription} />
You can use the "useState" hook as mentioned in the above answer or you can also use the 'useRef' hook as it can be used to hold values similar to an instance property on a class. You can read more about it here.
To use it in this case, you can do something like this:-
const presetDescription = useRef('');
axios.get(API_SEARCH_URL)
.then
(response => {
const preset = response.data;
presetDescription.current = preset[0].preset_description;
})
<PromptForm preset_out={presetDescription.current} />
Related
Imagine I have a hierarchy of react components, e.g. meals of the day
|------- breakfast --------|
| Meal1_image.png banana |
| Meal2_image.png porridge|
|------- lunch-------------|
| Meal3_image.png toast |
| Meal4_image.png apple |
I can add meals to a meal group (e.g. to the lunch group), I can add more lunch groups (e.g. midday snack) and so on.. it's a variable list of components and I give the user the ability to add meals and meal groups with '+' buttons, or to delete them and so on.
How would I go to save these in a text json and to read them back?
I read about "serializing react components" but maybe I don't need all of that stuff, I would just need to save a json like
{
"breakfast": {
{"food": "banana", "image": "Meal1_image.png"},
{"food": "porridge", "image": "Meal2_image.png"},
},
"lunch" : ... and so on ...
}
is there any simple way to achieve this or should I just go with components serialization with things like https://github.com/zasource-dev/React-Serialize ?
Question is basically too wide, but anyway, just split your task by subtasks.
Design the data models you are going to work with. Create a prototype of it, try to render it as is. Regarding your prototype - that is not valid js object, so i changed it a bit in my example. Note - for .map methods you need to use unique keys.
Figure out what will be the best place to keep your data (in state). You can store it in some component, you can store it in context and add all the needed methods to it and pass them down to your components. I did everything in a component for simplicity of example.
Add download/upload/parsing functions and wire everything up.
If I got your question right - you want each Client to be able to download and upload the Data to a local JSON file on Client, so no server interaction.
const { useState } = React;
function download(content, mimeType, filename) {
const a = document.createElement("a");
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
a.setAttribute("href", url);
a.setAttribute("download", filename);
a.click();
}
function upload(event, callback) {
const input = event.target;
const reader = new FileReader();
reader.onload = function () {
const text = reader.result;
callback(text);
};
reader.readAsText(input.files[0]);
}
const DEFAULT_DATA = {
breakfast: [
{ food: "banana", image: "Meal1_image.png" },
{ food: "porridge", image: "Meal2_image.png" }
],
lunch: [{ food: "banana", image: "Meal1_image.png" }]
};
function App() {
const [data, setData] = useState(DEFAULT_DATA);
const onDownloadClick = (e) => {
download(JSON.stringify(data), "application/json", "file1.json");
};
const onUpload = (e) => {
upload(e, (text) => setData(JSON.parse(text)));
};
return (
<div className="App">
{Object.entries(data).map(([section, items]) => (
<div key={section}>
<p>{section}</p>
<ul>
{items.map((item) => (
<li key={item.food}>
<p>{item.food}</p>
</li>
))}
</ul>
</div>
))}
<button onClick={onDownloadClick} type="button">
Download JSON
</button>
<input type="file" accept="application/json" onChange={onUpload} />
</div>
);
}
// v18.x+
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>
<div id="root"></div>
How to test - Download JSON file, open it and change something in it, like "banana" to "banana111", save, upload the file back and observe the updated values.
Not sure what is the plan about Images, but in worst case - you can store images in JSON as base64 strings.
If you mean physical saving in a file, then you need to have node js to access the file system of your phone or PC, your server:
// file system module to perform file operations
const fs = require('fs');
// json data
var jsonData = '{"persons":[{"name":"John","city":"New York"},{"name":"Phil","city":"Ohio"}]}';
// parse json
var jsonObj = JSON.parse(jsonData);
console.log(jsonObj);
// stringify JSON Object
var jsonContent = JSON.stringify(jsonObj);
console.log(jsonContent);
fs.writeFile("output.json", jsonContent, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
If we are talking about transformation for axios request, then you need to use the following javascript methods JSON.stringify() for json transformation and JSON.parse() for reading to transformation in javascript.
If you just want to transform the object in the hook state, this is also fine.
So I'm taking a course in web programming and in it we've gotten this assignment to design some simple front end for ordering salads, to get all the components etc. it was previously stored in a .js file in the following fashion
let inventory = {
Sallad: {price: 10, foundation: true, vegan: true},
Pasta: {price: 10, foundation: true, gluten: true},
'Salad + Pasta': {price: 10, foundation: true, gluten: true},
'Salad + Matvete': {price: 10, foundation: true, vegan: true, gluten: true},
'Kycklingfilé': {price: 10, protein: true},
'Rökt kalkonfilé': {price: 10, protein: true},
'Böngroddar': {price: 5, extra: true, vegan: true},
'Chèvreost': {price: 15, extra: true, lactose: true},
Honungsdijon: {price: 5, dressing: true, vegan: true},
Kimchimayo: {price: 5, dressing: true},
.
.
.
};
export default inventory;
This is then imported into my App.js that was created when creating the react project and sent as a prop to another component that took care of the composing of a salad that was eventually sent back to a function also sent with as a prop.
So what we're supposed to do now is to get this inventory from a local rest(?) server instead. So if I go to
http://localhost:8080/proteins
it will open a page that just displays an array with all the different choices of proteins
["Kycklingfilé","Rökt kalkonfilé","Norsk fjordlax","Handskalade räkor från Smögen","Pulled beef från Sverige","Marinerad bönmix"]
And then going to
http://localhost:8080/proteins/Kycklingfilé
Will give you another page with the properties of that ingredient
{"price":10,"protein":true}
And my attempt at recreating that inventory object with all the ingredients as properties inside state is this
class App extends Component {
constructor(props) {
super(props);
this.state = {
salads: [],
inventory: {
}
};
}
componentDidMount() {
const base = "http://localhost:8080/";
const pURL = base + "proteins/";
const fURL = base + "foundations/";
const eURL = base + "extras/";
const dURL = base + "dressings/";
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + e).then(response => response.json()).then(data => {
Object.assign(this.state.inventory, {e : data})
})
})
});
fetch(pURL).then(response => response.json()).then(data => this.setState({data}));
fetch(eURL).then(response => response.json()).then(data => this.setState({data}));
fetch(dURL).then(response => response.json()).then(data => this.setState({data}));
}
I've been using
{JSON.stringify(this.state)}
to try and look at whats going on and with this code it comes out as this
{"salads":[],"inventory":{},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So the fetch works fine for getting all the ingredients of a certain type, I guess it's only the dressings since it overwrites data each time on those last three fetches. But the problem is that inventory is completely empty.
If I instead write it like this
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
Object.assign(this.state.inventory, {e: fetch(fURL + e).then(response => response.json().then())})
})
});
The output becomes
{"salads":[],"inventory":{"e":{}},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So it adds the 'e' object, which is another problem since I want it to be the value of the current element, but it's completely empty, and I dont know how to get the data from that seconds fetch when I write it like that. So that's why it now looks like it does in the first code snippet, where it doesn't even get an empty 'e' inside inventory.
Finally, if I write it like that second example but just e: e like this
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
Object.assign(this.state.inventory, {e: e})
})
});
The output becomes
{"salads":[],"inventory":{"e":"Salad + Quinoa"},"data":["Ceasardressing","Dillmayo","Honungsdijon","Kimchimayo","Pesto","Rhodeisland","Rostad aioli","Soyavinägrett","Örtvinägrett"]}
So it seems like everything is working up until the .forEach on the array of strings that represents a certain type of ingredient since it manages to put that into 'e' inside inventory with one of the array elements as it's value. It's only the last one in the list though but I guess that stems from the problem that it just makes the object 'e' and not the value of the current element and overwrites it for every item.
Sorry if all the rambling made the problem unclear, but what I'm trying to achieve is inventory {} inside state that looks like it did when it was in a seperate file, so that when we create the component we can send this.state.inventory instead of the imported inventory as prop. And to create that using what we can fetch from the different pages.
When you write
{e : data}
you create a new Object with a single entry. That sets the value of the key 'e' as the current value of the variable 'data'. A variable named 'e' is not involved:
const e = 'hello';
console.log(e); // "hello"
console.log({ asd: e }); // { asd: "hello" }
console.log({ e: "asd" }); // { e: "asd" }
console.log({ e: asd }); // ReferenceError: asd is not defined
What you are trying to do is using the value of the variable e as the key that you want to set. In javascript this is done using [ and ] like so:
const e = 'hello';
console.log({ [e]: "world" }); // { hello: "world" }
// this is necessery whenever you want a key that is not a simple word
console.log({ ["key with spaces"]: "world" }); // { "key with spaces": "world" }
console.log({ [e + e]: "world" }); // { hellohello: "world" }
EDIT:
there is another issue with your code above that you might encounter sooner or later:
In React you should never ever modify this.state directly. Always go through this.setState()!
https://reactjs.org/docs/state-and-lifecycle.html#using-state-correctly
In your case this is a bit more difficult, since you are making multiple requests which each affect the same key in your state (inventory).
Because you cannot know in what order the requests arrive, and whether React will actually do the setState each time new data comes, or do them all at the same time, you cannot simply use this.setState({ inventory: newInventory }). Instead you should use the function version as described here. Unfortunately this can be a bit complex to grasp in the beginning :(
in your case I would solve it like this:
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + e)
.then(response => response.json())
.then(data => this.setState((prevState) => ({
inventory: Object.assign({}, prevState.inventory, {[e]: data}),
})));
})
})
});
A couple of things to note here:
note the ({ in (prevState) => ({ ... }): this is an arrow function that returns an object
we are passing a function to this.setState (see the link above for details). This function receives the current state as an argument (prevState) and should return the new State. (although it can omit keys of the old state that remain unchanged). This is better than directly passing the new state to this.setState because when multiple setState happen at the same time, React can apply the functions you pass in the right order so that all changes happen, but if you passed objects it has to decide on one of them to 'win' so changes can get lost.
In Object.assign({}, prevState.inventory, {[e]: data}), instead of modifying prevState.inventory we create a new object that contains the updated inventory. You should never modify the old state, even in this.setState.
Hope this helps :)
So with #sol's advice to use [e] to create the objects for each ingredient, this code
fetch(fURL).then(response => response.json()).then(data => {
data.forEach(e => {
fetch(fURL + [e]).then(response => response.json()).then(data => {
Object.assign(this.state.inventory, {[e] : data})
})
})
});
now works. I think why it didn't look successful with my "troubleshooting" of just printing that JSON.stringify of the entire state in render was that is just didn't render properly when react refreshed after saving the code. Updating the page makes it all blank, but clicking onto another page through a link and then back fixes it. Dont know why, but I'll take it.
I am trying to make $onChanges hook work by using immutable way.
Chat Service
class ChatService {
constructor() {
this.collection = {
1: [
{
chat: 'Hi',
},
{
chat: 'Hello',
},
{
chat: 'How are you?',
},
],
};
}
getCollection() {
return this.collection;
}
getChatById(id) {
return this.collection[id];
}
addChat(id, chat) {
// this.collection[id].push(chat);
this.collection[id] = this.collection[id].concat(chat);
}
}
Chat Component
const Chat = {
bindings: {},
template: `<chat-list chats="$ctrl.chats" add-msg="$ctrl.addMsg(chat)"></chat-list>`,
// template: `<chat-list chats="$ctrl.chats[$ctrl.id]" add-msg="$ctrl.addMsg(chat)"></chat-list>`,
controller: class Chat {
constructor(ChatService) {
this.ChatService = ChatService;
this.id = 1;
// if i get the all the chat collection by
// this.chats = ChatService.getCollection()
// and then use like above in the commented out template,
// and it works and triggers $onChanges
this.chats = ChatService.getChatById(this.id);
}
addMsg(msg) {
this.ChatService.addChat(this.id, { chat: msg });
}
},
};
Chat List Component
const ChatList = {
bindings: {
chats: '<',
addMsg: '&',
},
template: `
<div>
<li ng-repeat="chat in $ctrl.chats">{{chat.chat}}</li>
<form ng-submit="$ctrl.addMsg({chat: chatmodel})">
<input ng-model="chatmodel">
</form>
</div>
`,
controller: class ChatList {
$onChanges(changes) {
console.log(changes);
if (changes.chats && !changes.chats.isFirstChange()) {
// this.chats = changes.chats.currentValue;
}
}
},
};
However, $onChanges hook doesn't fire. I know that in order to make the $onChanges fire, need to break the reference of binding chats in chat-list component from the chat component.
Also I could re-fetch the chats after adding on the addMsg method, it would work and trigger $onChanges but if the msg was from the another user and lets say if I was using Pusher service, it would only update the chats collection on the Chat Service not the chat-list component.
One way $onChanges seems to fire is when I get all the chat collection and then use ctrl.id to get particular chats when passing via the bindings like <chat-list chats="$ctrl.chats[$ctrl.id]" instead of <chat-list chats="$ctrl.chats. However, this will update chat list without doing anything on the $onChanges.
Ideally, I would like to update the chat list on the view by <chat-list chats="$ctrl.chats and then using the currentValue from the $onChanges hook and not use like $watch and $doCheck. I am not sure how to do it. Any help is appreciated. Thanks and in advance.
Here's very basic example of it on the plunkr.
Let's walk trough what your code is doing for a minute to ensure we understand what's going wrong:
The constructor in ChatServices creates a new object in memory (Object A), this object has a property 1 which holds an array in memory (Array 1)
constructor() {
this.collection = {
1: [
{
chat: 'Hi',
},
{
chat: 'Hello',
},
{
chat: 'How are you?',
},
],
};
}
In your component's constructor, you use the ChatService to retrieve Array 1 from memory and store it in the this.chats property from your component
this.chats = ChatService.getChatById(this.id);
So currently, we have two variables pointing to the same array (Array 1) in memory: The chats property on your component and the collection's 1 property in the ChatService.
However, when you add a message to the ChatService, you are using the following:
addChat(id, chat) {
this.collection[id] = this.collection[id].concat(chat);
}
What this is doing is: It updates collection's 1 property to not point towards Array 1, but instead creates a new array by concatenating both the current Array 1 and a new message, store it in memory (Array 2) and assign it to collection[id].
Note: This means the Object A object's 1 property also points to Array 2
Even tho the collection's 1 property has been updated properly when it comes to immutability, the chats property on your component is still pointing towards Array 1 in memory.
There's nothing indicating it should be pointing to Array 2.
Here's a simple example demonstrating what's happening:
const obj = { 1: ['a'] };
function get() {
return obj['1'];
}
function update() {
obj['1'] = obj['1'].concat('b');
}
const result = get();
console.log('result before update', result );
console.log('obj before update', obj['1']);
update();
console.log('result after update', result );
console.log('obj after update', obj['1']);
As you can see in the above snippet, pointing obj['1'] towards a new array doesn't change the array result points to.
This is also why the following is working correctly:
One way $onChanges seems to fire is when I get all the chat collection
and then use ctrl.id to get particular chats when passing via the
bindings like <chat-list chats="$ctrl.chats[$ctrl.id]" instead of
<chat-list chats="$ctrl.chats.
In this case you are storing a reference to Object A. As mentioned above, the 1 property on the ChatService's collection is updated correctly, so this will reflect in your component as it's also using that same Object A.
To resolve this without using the above way (which is, passing Object A to your component), you should ensure the component is aware of the changes made to Object A (as it can not know this when not having access to it).
A typical way these kind of things are done in Angular (I know this is AngularJS, but just pointing out how you can resolve this in a way Angular would do and works fine with Angular JS) is by using RXjs and subscribe to the chats changes in your component.
I'm trying to render a message to a span tag specific to an item in a list. I've read a lot about React 'refs', but can't figure out how to populate the span with the message after it's been referenced.
So there's a list of items and each item row has their own button which triggers an API with the id associated with that item. Depending on the API response, i want to update the span tag with the response message, but only for that item
When the list is created the items are looped thru and each item includes this
<span ref={'msg' + data.id}></span><Button onClick={() => this.handleResend(data.id)}>Resend Email</Button>
After the API call, I want to reference the specific span and render the correct message inside of it. But I can't figure out how to render to the span at this point of the code. I know this doesn't work, but it's essentially what I am trying to do. Any ideas?
if (response.status === 200) {
this.refs['msg' + id] = "Email sent";
I recommand using state. because string refs legacy (https://reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs)
const msgs = [
{ id:1, send:false },
{ id:2, send:false },
{ id:3, send:false },
];
this.state = {
msgs
};
return this.state.msgs.map((msg, index) => {
const status = msg.send ? "Email Sent" : "";
<span>{ status }</span><Button onClick={() => this.handleResend(index)}>Resend Email</Button>
});
async handleResend (index) {
const response = await callAPI(...);
if(reponse.status !== 200) return;
const newMsgs = _.cloneDeep(this.state.msgs);
newMsgs[index].send = true;
this.setState({
msgs: newMsgs
})
}
The workaround is set innerText
this.refs['msg' + id].innerText = "Email sent";
But rather than using ref try to use state to update elements inside render.
i was facing with this issue right now and i figured it out this way:
// currentQuestion is a dynamic Object that comes from somewhere and type is a value
const _target = `${currentQuestion.type}_01`
const _val = this[`${_target}`].current.clientHeight // here is the magic
please note that we don't use . after this to call the ref and not using refs to achieve what we want.
i just guessed that this should be an Object that would hold inner variables of the current object. then since ref is inside of that object then we should be able to call it using dynamic values like above...
i can say that it worked automagically!
I'm trying to render some rows from firebase database, I'm getting this error:
TaskQueue: Error with task : Invariant Violation: Tried to get frame
for out of range index NaN
const { currentUser } = firebase.auth();
var userfavorites = firebase.database().ref(`/users/${currentUser.uid}/favorites/`);
userfavorites.once('value').then(snapshot => {
this.setState({ userfav: snapshot.val() })
})
...
<FlatList
data={this.state.userfav}
renderItem={({ item }) => (
<Text>{item.favdata}</Text>
)}
/>
I came across this error, I had a PHP back-end and trying to get json_encoded data into a FlatList.
Problem: The REST endpoint was returning an object eg
{"Total_Excavator":2,"Active_Excavator":2,"Total_load":6804}
Solution: Fixed it to return an array rather eg
[{"Total_Excavator":2,"Active_Excavator":2,"Total_load":6804}]
Take note of the Square Brackets.
I used $data[] = json_encode($excavatorArray) instead of
$data = json_encode($excavatorArray)
. Hope it helps someone one day
I had the same issue, it seems this problem is the reason of the object names.
In the image below you can see that as soon as you fetch snapshot from Firebase endpoint it comes with the id which is not recognized by React Native. And react acts like it's empty obj.
All you have to do is map the items of the object after fetching it like example below,
const fbObject = snap.val();
const newArr = [];
Object.keys(fbObject).map( (key,index)=>{
console.log(key);
console.log("||");
console.log(index);
newArr.push(fbObject[key]);
});
Just a slight modification to the answer from #mert. JavaScript's map operator returns an array; so, there is no need to push elements onto newArr. Also, the new array's elements are going to be missing the unique Firebase id. So, it should be added into the array element.
const fbObject = snapshot.val();
const newArr = Object.keys(fbObject).map((key) => {
fbObject[key].id = key;
return fbObject[key];
});
You'll end up with a newArray like this:
[
{
"id": "12345",
"name": "Jane Doe",
"age": 18
},
{
"id": "23456",
"name": "John Smith",
"age": 27
}
]
I had a similar problem and saved the flatlist's data in the state. You have to make sure that the data in the state is a list instead of an object:
this.state = {
...,
data: [], // <-- wrap your flatlist's data in there
}
When making changes to your data/the state, always use the callback function to perform something afterwards to keep your UI in sync:
this.setState({..., data}, () => yourCallback())