I'm trying to create an 'object of objects' in ReactJS using state hooks, but I'm unsure how to dynamically create it based on the data coming in.
The data arrives on a websocket, which I have placed in a Context and is being used by the component in question. The JSON data hits the onmessage, it invokes my useEffect state hook to then call a function to update the useState variable accordingly.
The inbound websocket data messages come in one at a time and look something like this (important keys listed, but there lots more props inside them) :
{
"name": "PipelineA",
"state": "succeeded",
"group": "Group1"
}
{
"name": "PipelineE",
"state": "succeeded",
"group": "Group1"
}
{
"name": "PipelineZ",
"state": "succeeded",
"group": "Group4"
}
...where the name and group are the values I want to use to create an 'object of objects'. So the group will be used to create a group of pipelines that are all part of that same group, which within that object, each pipeline will have its name as the 'key' for its entire data. So, the end state of the ‘object of objects’ would look something like this:
{
"Group1": {
"PipelineA": {
"name": "PipelineA",
"state": "running",
"group": "Group1"
},
"PipelineB": {
"name": "PipelineB",
"state": "running",
"group": "Group1"
}
},
"Group2": {
"PipelineC": {
"name": "PipelineC",
"state": "running",
"group": "Group2"
},
"PipelineD": {
"name": "PipelineD",
"state": "running",
"group": "Group2"
}
},
...etc...
}
So the idea being, pipelines of Group1 will be added to the Group1 object, if PipelineA already exists, it just overwrites it, if it does not, it adds it. And so on and so on.
I'm (somewhat) fine with doing this outside of React in plain JS, but I cannot for the life of me figure out how to do it in ReactJS.
const [groupedPipelineObjects, setGroupedPipelineObjects] = useState({});
const [socketState, ready, message, send] = useContext(WebsocketContext);
useEffect(() => {
if (message) {
updatePipelineTypeObjects(message)
}
}, [message]);
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name
const pipelineGroup = data.group
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects(prevState => ({
...prevState,
[pipelineGroup]: {[pipelineName]: data} // <-- doesnt do what I need
}))
}
And help or suggestions would be appreciated. FYI the pipeline names are unique so no duplicates, hence using them as keys.
Also, why am I doing it this way? I already have it working with just an object of all the pipelines where the pipeline name is the key and its data is the value, which then renders a huge page or expandable table rows. But I need to condense it and have the Groups as the main rows for which I then expand them to reveal the pipelines within. I thought doing this would make it easier to render the components.
It's just that you haven't gone quite far enough. What you have will replace the group entirely, rather than just adding or replacing the relevant pipeline within it. Instead, copy and update the existing group if there is one:
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name;
const pipelineGroup = data.group;
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects((prevState) => {
const groups = { ...prevState };
if (groups[pipelineGroup]) {
// Update the existing group with this pipeline,
// adding or updating it
groups[pipelineGroup] = {
...groups[pipelineGroup],
[pipelineName]: data,
};
} else {
// Add new group with this pipeline
groups[pipelineGroup] = {
[pipelineName]: data,
};
}
return groups;
});
};
Also, you're trying to use iterable destructuring ([]) here:
const [ socketState, ready, message, send ] = useContext(WebsocketContext);
but as I understand it, your context object is a plain object, not an iterable, so you'd want object destructuring ({}):
const { socketState, ready, message, send } = useContext(WebsocketContext);
Live Example:
const { useState, useEffect, useContext } = React;
const WebsocketContext = React.createContext({ message: null });
const Example = () => {
const [groupedPipelineObjects, setGroupedPipelineObjects] = useState({});
const { socketState, ready, message, send } = useContext(WebsocketContext);
useEffect(() => {
if (message) {
updatePipelineGroupObjects(message);
}
}, [message]);
const updatePipelineGroupObjects = (data) => {
const pipelineName = data.name;
const pipelineGroup = data.group;
// let groupObj = {pipelineGroup: {}} // do I need to create it first?
setGroupedPipelineObjects((prevState) => {
const groups = { ...prevState };
if (groups[pipelineGroup]) {
// Update the existing group with this pipeline,
// adding or updating it
groups[pipelineGroup] = {
...groups[pipelineGroup],
[pipelineName]: data,
};
} else {
// Add new group with this pipeline
groups[pipelineGroup] = {
[pipelineName]: data,
};
}
return groups;
});
};
return <pre>{JSON.stringify(groupedPipelineObjects, null, 4)}</pre>;
};
// Mocked messages from web socket
const messages = [
{
name: "PipelineA",
state: "succeeded",
group: "Group1",
},
{
name: "PipelineB",
state: "running",
group: "Group1",
},
{
name: "PipelineC",
state: "running",
group: "Group2",
},
{
name: "PipelineD",
state: "running",
group: "Group2",
},
{
name: "PipelineE",
state: "succeeded",
group: "Group1",
},
{
name: "PipelineZ",
state: "succeeded",
group: "Group4",
},
];
const App = () => {
const [fakeSocketContext, setFakeSocketContext] = useState({ message: null });
useEffect(() => {
let timer = 0;
let index = 0;
tick();
function tick() {
const message = messages[index];
if (message) {
setFakeSocketContext({ message });
++index;
timer = setTimeout(tick, 800);
}
}
return () => {
clearTimeout(timer);
};
}, []);
return (
<WebsocketContext.Provider value={fakeSocketContext}>
<Example />
</WebsocketContext.Provider>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Related
I have retrieved data from an API, and now trying to transform the data to send a POST request. I want to group two User ID's that match, and POST their common cities in a array instead of separate objects.
For example, data I retrieve looks like this:
{
"events": [
{
"city": "city-1",
"user": "d1177368-2310-11e8-9e2a-9b860a0d9039"
},
{
"city": "city-2",
"user": "d1177368-2310-11e8-9e2a-9b860a0d9039"
}
]}
I want my POST request data to look similar to this:
{
"user": {
"d1177368-2310-11e8-9e2a-9b860a0d9039": [
{
"city": [
"city-1",
"city-2"
]
}]}}
So far this is my React component for the request:
import React, { useEffect, useState } from "react";
import axios from "../data/axios";
export default function Events({ fetchEvents }) {
const [events, setEvents] = useState([]);
useEffect(() => {
async function fetchData() {
const requests = await axios.get(fetchEvents);
setEvents(requests.data.events);
return requests;
}
fetchData();
}, [fetchEvents]);
//here is my issue:
function createSessions(user, city) {
if (user === user) {
}
}
Thank you
Iterate over the events array, reducing it into an object with a user object property. The user object has the user values from the events array elements as key and the cities are pushed into a city array property.
events.reduce(
(result, el) => {
if (!result.user[el.user]) {
result.user[el.user] = [{ city: [] }];
}
result.user[el.user][0].city.push(el.city);
return result;
},
{ user: {} }
);
const data = {
events: [
{
city: "city-1",
user: "d1177368-2310-11e8-9e2a-9b860a0d9039"
},
{
city: "city-2",
user: "d1177368-2310-11e8-9e2a-9b860a0d9039"
}
]
};
const data2 = data.events.reduce(
(result, el) => {
if (!result.user[el.user]) {
result.user[el.user] = [{ city: [] }];
}
result.user[el.user][0].city.push(el.city);
return result;
},
{ user: {} }
);
console.log(data2);
I want to update an state object userInfo which contains an array named work which is an array of objects
My Database object userInfo looks like this
{
"_id":"61a6a1d64707c03465eae052",
"fullName": "Md Nurul Islam",
"works":
[
{
"isEditing": false,
"_id": "61a6a1d64707c03465eae053",
"company": "Amazon Logistics",
"position": "Sortation Associate",
"isCurrent": true
},
{
"isEditing": false,
"_id":"61a6a1d64707c03465eae054",
"company": "The Rani Indian Takeway",
"position": "Customer Service Assistant",
"isCurrent": false,
}
]
}
But I don't want update the database, i just want to update the state userInfo in frontend to go editing mood, for this when the user clicks edit next to work section, the properties isEditing will be toggled.
Below is my code
const [userInfo, setUserInfo] = useState(user);
const workToggleHandler = (work) =>
{
setUserInfo((prev) => {
prev.works.map((p) => {
if (p._id === work._id) {
p.isEditing = !p.isEditing;
}
return p;
});
return prev;
});
With this code, my state object is updated But my components are not re-rendered
you can put into the useEffect when particular state is updated then that effect will call
useEffect(()=>{
your code will go here
}[userInfo]),
I just changed my code on workToggleHandler function and its working completely as i want.
Below my code
const [userInfo, setUserInfo] = useState(user);
const workToggleHandler = (work) => {
setUserInfo({
...userInfo,
works: userInfo.works.map((p) => {
if (p._id === work._id) {
p.isEditing = !p.isEditing;
}
return p;
}),
});
};
React does not look at the data of the object but rather on the pointer of the object. Since this is not changed (only "prev" is edited, no new object with a new object address), it doesn't not rerender.
A solution would be to copy the object.
let newUserInfo = Object.assign({}, prev);
newUserInfo.works.map((p) => {
if (p._id === work._id) {
p.isEditing = !p.isEditing;
}
return p;
});
return newUserInfo;
Disclaimer: Maybe a deep copy is necessary....
I'm trying to clean up the data received from firebase to view them in a FlatList. How can I clean my data to a simple array where I can iterate in FlatList?
EDIT! There are many other coins in my database that I want to pull into the FlatList. So the solution that I'm looking for is to view all these coins in my FlatList and then show their data such as price, market_cap etc.
My data is currently stored in a state and looks like this.
favoriteList data is:
Object {
"bitcoin": Object {
"-MahI1hCDr0CJ_1T_umy": Object {
"data": Object {
"ath": 54205,
"ath_change_percentage": -40.72194,
"ath_date": "2021-04-14T11:54:46.763Z",
"atl": 51.3,
"atl_change_percentage": 62536.71794,
"atl_date": "2013-07-05T00:00:00.000Z",
"circulating_supply": 18719656,
"current_price": 32164,
"fully_diluted_valuation": 674764316483,
"high_24h": 33004,
"id": "bitcoin",
"image": "https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579",
"last_updated": "2021-05-27T10:07:02.525Z",
"low_24h": 30652,
"market_cap": 601493137412,
"market_cap_change_24h": -15118857257.119507,
"market_cap_change_percentage_24h": -2.45192,
"market_cap_rank": 1,
"max_supply": 21000000,
"name": "Bitcoin",
"price_change_24h": -641.85835686,
"price_change_percentage_1h_in_currency": 0.25769270475453127,
"price_change_percentage_24h": -1.95655,
"price_change_percentage_24h_in_currency": -1.9565521832416402,
"price_change_percentage_7d_in_currency": 4.978932125496787,
"symbol": "btc",
"total_supply": 21000000,
"total_volume": 36947814578,
},
},
},
}
The firebase structure is like this where the data above is fetched from:
Object.keys(favourite.bitcoin)[idx] This line gives you the name of key at index 0 into object favourite.bitcoin.
So the variable key will be your firebase key.
let favourite = {
bitcoin: {
"-MahI1hCDr0CJ_1T_umy": {
data: {
ath: 54205,
ath_change_percentage: -40.72194,
ath_date: "2021-04-14T11:54:46.763Z",
atl: 51.3,
atl_change_percentage: 62536.71794,
atl_date: "2013-07-05T00:00:00.000Z",
circulating_supply: 18719656,
current_price: 32164,
fully_diluted_valuation: 674764316483,
high_24h: 33004,
id: "bitcoin",
image:
"https://assets.coingecko.com/coins/images/1/large/bitcoin.png?1547033579",
last_updated: "2021-05-27T10:07:02.525Z",
low_24h: 30652,
market_cap: 601493137412,
market_cap_change_24h: -15118857257.119507,
market_cap_change_percentage_24h: -2.45192,
market_cap_rank: 1,
max_supply: 21000000,
name: "Bitcoin",
price_change_24h: -641.85835686,
price_change_percentage_1h_in_currency: 0.25769270475453127,
price_change_percentage_24h: -1.95655,
price_change_percentage_24h_in_currency: -1.9565521832416402,
price_change_percentage_7d_in_currency: 4.978932125496787,
symbol: "btc",
total_supply: 21000000,
total_volume: 36947814578,
},
},
},
};
let idx = 0; //key at index 0
let key = Object.keys(favourite.bitcoin)[idx];
console.log(key)
let data = favourite.bitcoin[key].data;
console.log(data)
Please let me know if it's works or not !
To get the data from your database, you need to query its parent reference. This will allow you to do things like "find all entries under /favorites/bitcoin that have a current price of over 30000".
Because you want to simply query for all the data under /favorites/bitcoin in your question, you would do the following:
Get a reference for /favorites/bitcoin
Get the data under /favorites/bitcoin
Iterate over the data, and assemble an array
Use this new array for your FlatList
These steps can be made into the following function:
function getDataForFlatlistUnder(databasePath) {
return firebase.database()
.ref(databasePath)
// consider using .limitToFirst(10) or similar queries
.once("value")
.then((listSnapshot) => {
// listSnapshot contains all the data under `${databasePath}`
const arrayOfDataObjects = [];
// For each entry under `listSnapshot`, pull its data into the array
// Note: this is a DataSnapshot#forEach() NOT Array#forEach()
listSnapshot.forEach((entrySnapshot) => {
// entrySnapshot contains all the data under `${databasePath}/${entrySnapshot.key}`
const data = entrySnapshot.child("data").val();
// data is your data object
// i.e. { ath, ath_change_percentage, ath_date, atl, ... }
// add the key into the data for use with the FlatList
data._key = entrySnapshot.key;
arrayOfDataObjects.push(data);
});
return arrayOfDataObjects;
});
}
Which you can use in your component like so:
function renderItem((dataObject) => {
// TODO: render data in dataObject
});
function MyComponent() {
const [listData, setListData] = useState();
const [listDataError, setListDataError] = useState(null);
const [listDataLoading, setListDataLoading] = useState(true);
useEffect(() => {
const disposed = false;
getDataForFlatlistUnder("favorites/bitcoin")
.then((arrayOfDataObjects) => {
if (disposed) return; // component was removed already, do nothing
setListData(arrayOfDataObjects);
setListDataLoading(false);
})
.catch((err) => {
if (disposed) return; // component was removed already, do nothing
// optionally empty data: setListData([]);
setListDataError(err);
setListDataLoading(false);
});
// return a cleanup function to prevent the callbacks above
// trying to update the state of a dead component
return () => disposed = true;
}, []); // <-- run this code once when component is mounted and dispose when unmounted
if (listDataLoading)
return null; // or show loading spinner/throbber/etc
if (listDataError !== null) {
return (
<Text>
{"Error: " + listDataError.message}
</Text>
);
}
return (
<FlatList
data={listData}
renderItem={renderItem}
keyExtractor={item => item._key} // use the key we inserted earlier
/>
);
}
Note: This code is for a one-off grab of the data, if you want realtime updates, you would modify it to use .on("value", callback) instead. Make sure to use .off("value", callback) in the unsubscribe function of the useEffect call to clean it up properly.
It is interesting to see how programmers interpret questions. Or perhaps how beginners fail to articulate clearly what they want to achieve. Here is the answer:
const formatData = (data) => {
let arr = [];
let test = Object.values(data).forEach((o) => {
Object.values(o).forEach((a) =>
Object.values(a).forEach((b) => arr.push(b))
);
setFormattedData(arr);
});
I would be able to update an object nested in another object in my application but i got some problems.
Let's assume that the entity that I want to update is something like this:
{
{
"id": 0,
"name": "Yellow Car",
"details": {
"engine": {},
"ownerInfo": {
"name": "Luke",
"lastName": "Cage",
"email": "l.cage#hisemail.blabla"
},
},
"created": "2018-01-17",
"lastUpdate": "2020-09-03",
}
I can easily update some part of this entity in this way:
let car: Car = {
...car,
...this.form.value
};
let carUpdate: Update<Car> = {
id: car.id,
changes: car
};
this.store.dispatch(carUpdated({carUpdate}));
But in this way I can only update name, created, lastUpdate and I can't update the nested object details. What happens if I try to edit the detail object now? Nothing i wanna happens.
This is the selector:
export const carUpdated = createAction(
"[Edit Car] Car Updated",
props<{carUpdate: Update<Car>}>()
);
The effect:
saveCar$ = createEffect(
() => this.actions$
.pipe(
ofType(CarActions.carUpdated),
concatMap(action => this.carService.editCar(
action.carUpdate.changes,
action.carUpdate.id
))
),
{dispatch: false}
)
The reducer:
on(CarActions.carUpdated, (state, action) =>
adapter.updateOne(action.carUpdate, state)),
The service sends to the backend the right data and it's working good without the state management.
What I am doing now is retrieve the single carObject in this way in the component in the ngOnInit
car$ = this.store.pipe(
select(selectCar(id))
)
and the selector is:
export const selectCar = (id) => createSelector(
selectAllCars,
(cars: any) => cars.filter((car) => {
let filteredCar = car.id == id;
if(filteredCar) {
return car;
}
}).map(car => car)
);
and then after my edit I can use the dispatch to confirm my edit
this.store.dispatch(carUpdated({carUpdate}));
but as I said if I try to update the details object i have this
let car: Car = {
...car, // the entire car object
...this.form.value // only the details
};
let carUpdate: Update<Car> = {
id: car.id,
changes: car //the details are mixed with the car object and not inside the object itself
};
something like this:
{
"id": 0,
"name": "Yellow Car",
"engine": {},
"details": {
},
"ownerInfo": {
"name": "Luke",
"lastName": "Cage",
"email": "l.cage#hisemail.blabla"
},
"created": "2018-01-17",
"lastUpdate": "2020-09-03",
}
Is there an easy way to fix this?
I'll try to provide a general answer which might show techniques on how to update one object with another object.
If in the form, you only have the details as update (update is Partial<Car["details"]>, you can do this:
const update: Partial<Car["details"]> = this.form.value;
const newCar: Car = {
...oldCar,
details: {
...oldCar.details,
...update
}
Then there's the possibility that the update is (partial) Car with partial details:
const update: Partial<Car> & { details: Partial<Car["details"]> } = this.form.value;
const newCar: Car = {
...oldCar,
...update,
details: {
...oldCar.details,
...update.details
}
}
An improbable option is that you have mixed detail and car properties in the update (for whatever reason - you might be doing something wrong). Then you can pick them by hand. Note that this will delete old values if new values are undefined.
const update: Pick<Car, 'name' | 'lastUpdate'> & Pick<Car["details"], 'ownerInfo'> = this.form.value;
const newCar: Car = {
...oldCar,
name: update.name,
lastUpdate: update.lastUpdate,
name: update.name
details: {
...oldCar.details,
ownerInfo: details.ownerInfo
}
}
I’ve got a very deeply nested object in my React state. The aim is to change a value from a child node. The path to what node should be updated is already solved, and I use helper variables to access this path within my setState.
Anyway, I really struggle to do setState within this nested beast. I abstracted this problem in a codepen:
https://codesandbox.io/s/dazzling-villani-ddci9
In this example I want to change the child’s changed property of the child having the id def1234.
As mentioned the path is given: Fixed Path values: Members, skills and variable Path values: Unique Key 1 (coming from const currentGroupKey and both Array position in the data coming from const path
This is my state object:
constructor(props) {
super(props);
this.state = {
group:
{
"Unique Key 1": {
"Members": [
{
"name": "Jack",
"id": "1234",
"skills": [
{
"name": "programming",
"id": "13371234",
"changed": "2019-08-28T19:25:46+02:00"
},
{
"name": "writing",
"id": "abc1234",
"changed": "2019-08-28T19:25:46+02:00"
}
]
},
{
"name": "Black",
"id": "5678",
"skills": [
{
"name": "programming",
"id": "14771234",
"changed": "2019-08-28T19:25:46+02:00"
},
{
"name": "writing",
"id": "def1234",
"changed": "2019-08-28T19:25:46+02:00"
}
]
}
]
}
}
};
}
handleClick = () => {
const currentGroupKey = 'Unique Key 1';
const path = [1, 1];
// full path: [currentGroupKey, 'Members', path[0], 'skills', path[1]]
// result in: { name: "writing", id: "def1234", changed: "2019-08-28T19:25:46+02:00" }
// so far my approach (not working) also its objects only should be [] for arrays
this.setState(prevState => ({
group: {
...prevState.group,
[currentGroupKey]: {
...prevState.group[currentGroupKey],
Members: {
...prevState.group[currentGroupKey].Members,
[path[0]]: {
...prevState.group[currentGroupKey].Members[path[0]],
skills: {
...prevState.group[currentGroupKey].Members[path[0]].skills,
[path[1]]: {
...prevState.group[currentGroupKey].Members[path[0]].skills[
path[1]
],
changed: 'just now',
},
},
},
},
},
},
}));
};
render() {
return (
<div>
<p>{this.state.group}</p>
<button onClick={this.handleClick}>Change Time</button>
</div>
);
}
I would appreciate any help. I’m in struggle for 2 days already :/
Before using new dependencies and having to learn them you could write a helper function to deal with updating deeply nested values.
I use the following helper:
//helper to safely get properties
// get({hi},['hi','doesNotExist'],defaultValue)
const get = (object, path, defaultValue) => {
const recur = (object, path) => {
if (object === undefined) {
return defaultValue;
}
if (path.length === 0) {
return object;
}
return recur(object[path[0]], path.slice(1));
};
return recur(object, path);
};
//returns new state passing get(state,statePath) to modifier
const reduceStatePath = (
state,
statePath,
modifier
) => {
const recur = (result, path) => {
const key = path[0];
if (path.length === 0) {
return modifier(get(state, statePath));
}
return Array.isArray(result)
? result.map((item, index) =>
index === Number(key)
? recur(item, path.slice(1))
: item
)
: {
...result,
[key]: recur(result[key], path.slice(1)),
};
};
const newState = recur(state, statePath);
return get(state, statePath) === get(newState, statePath)
? state
: newState;
};
//use example
const state = {
one: [
{ two: 22 },
{
three: {
four: 22,
},
},
],
};
const newState = reduceStatePath(
state,
//pass state.one[1],three.four to modifier function
['one', 1, 'three', 'four'],
//gets state.one[1].three.four and sets it in the
//new state with the return value
i => i + 1 // add one to state.one[0].three.four
);
console.log('new state', newState.one[1].three.four);
console.log('old state', state.one[1].three.four);
console.log(
'other keys are same:',
state.one[0] === newState.one[0]
);
If you need to update a deeply nested property inside of your state, you could use something like the set function from lodash, for example:
import set from 'lodash/set'
// ...
handleClick = () => {
const currentGroupKey = 'Unique Key';
const path = [1, 1];
let nextState = {...this.state}
// as rightly pointed by #HMR in the comments,
// use an array instead of string interpolation
// for a safer approach
set(
nextState,
["group", currentGroupKey, "Members", path[0], "skills", path[1], "changed"],
"just now"
);
this.setState(nextState)
}
This does the trick, but since set mutates the original object, make sure to make a copy with the object spread technique.
Also, in your CodeSandbox example, you set the group property inside of your state to a string. Make sure you take that JSON string and construct a proper JavaScript object with it so that you can use it in your state.
constructor(props) {
super(props)
this.setState = { group: JSON.parse(myState) }
}
Here's a working example:
CodeSandbox