Problem with createActionThunk redux toolkit - javascript

Hello I made a createAsyncThunk function which is manipulating the actual state (it removes value with specific index). It executes but it doesn't update the firebase database and I'm not getting the value of categories array in the extraReducers. I don't know why because when I use console.log the categories array has got values. I'm using typescript and redux toolkit.
export const removeCategory = createAsyncThunk(
"categories/removeCategory",
async (index: number, thunkAPI) => {
const state = thunkAPI.getState() as RootState;
const categories = [...state.categories];
categories.splice(index, 1);
console.log(categories);
const updates = { categories: [] as string[] };
updates["categories"] = [...categories];
update(ref(database), categories);
return categories;
}
);

In this part of the code, are you sure you don't want to send the updates object instead of the categories?
const updates = { categories: [] as string[] };
updates["categories"] = [...categories];
update(ref(database), updates);
return categories;

Related

Looking for a pattern to normalize state in Recoil without losing the benefit of Suspense

In RecoilJS, seamless integration with React Suspense for async selectors is a big plus. However, I am running into issues trying to normalize the data cached in Recoil, while still making use of Suspense.
To explain the problem through an example, a User might have a collection of Books. A query populates the collection with a single API call to get all the user's "Favorite" books. A later query might simply request a single book, which may or may not have already been retrieved through the favorite books query.
What I'd like to do it maintain a normalized cache of Books, such as in an AtomFamily keyed by bookId, so I don't have two copies of books that are pulled with different queries. However, I run into a problem, which is that I would like to use Suspense for any one of the queries that retrieves one or more Books. And the natural way to do that with Recoil is to use an async Selector. But I don't see it, if there's a way to normalize the data fetched through async selectors.
Is there a pattern I am overlooking, that would allow me to use async selectors representing different queries that are backed by a shared, normalized AtomFamily?
For example, if I have this BAD code, which creates duplicate objects in my state, how might I rework it to maintain a shared cache for the actual Book objects, and still make use of Suspense if a query is still fetching when a component that uses this state renders?
Query 1: get a group of books through a selector:
const favoriteBooksSelector = selector({
key: 'MyFavoriteBooks',
get: async ({ get }) => {
const response = await allMyFavorityBooksDBQuery({
userID: get(currentUserIDState)
});
return response.books;
},
});
Query 2: get a single book, looks something like:
export const singleBookSelector = selectorFamily({
key: 'singleBookSelector',
get: (bookId: string) => async ({ get }) => {
const response = await singleBookDBQuery({
userID: get(currentUserIDState)
});
return response.book;
}
});
To utilize a cache, it must be indexed (keyed). For your example case, it is sensible to key a cache by book ID, so a KV cache is a reasonable choice. In JavaScript, a Map is a natural choice for such a cache.
Below, I have composed a fully-functional example of how to implement such a cache as a primary source for some Recoil atomFamily instances. The code is commented, and I can provide more explanation if anything is unclear.
An increasing query count is displayed as proof of the effectiveness of the cache. I have also included a link to the code in the TypeScript Playground for evaluation. If you would like to modify the code, all you need to do is copy it into a new answer (or just copy and paste it into local text editor and save it as an HTML file, and then serve it via a local http server).
TS Playground
<div id="root"></div><script src="https://unpkg.com/react#17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom#17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/recoil#0.6.1/umd/recoil.min.js"></script><script src="https://unpkg.com/#babel/standalone#7.17.6/babel.min.js"></script><script>Babel.registerPreset('tsx', {presets: [[Babel.availablePresets['typescript'], {allExtensions: true, isTSX: true}]]});</script>
<script type="text/babel" data-type="module" data-presets="tsx,react">
// import ReactDOM from 'react-dom';
// import {default as React, Suspense, useEffect, useState, type ReactElement, type ReactNode} from 'react';
// import {atomFamily, RecoilRoot, useRecoilValue} from 'recoil';
// This Stack Overflow snippet demo uses UMD modules instead of the above import statments
const {Suspense, useEffect, useState} = React;
const {atomFamily, RecoilRoot, useRecoilValue} = Recoil;
type Book = {
author: string;
id: string;
title: string;
};
// Database simulation:
// The simulated database
const db = new Map<string, Book>();
// Scraped from https://www.penguin.co.uk/articles/2018/100-must-read-classic-books.html#100
(JSON.parse(`[{"author":"Jane Austen","title":"Pride and Prejudice","id":"BnuQKALlW6B6sZNU4bdaB"},{"author":"Harper Lee","title":"To Kill a Mockingbird","id":"UM3ms9hlnTbEmx44JknKc"},{"author":"F. Scott Fitzgerald","title":"The Great Gatsby","id":"hBl51iaNCQ8qZw5iec8hD"},{"author":"Gabriel García Márquez","title":"One Hundred Years of Solitude","id":"CC9hIXCdEHR0beJlbMF_y"},{"author":"Truman Capote","title":"In Cold Blood","id":"l0iJfZNmNBfioHDnHARWQ"},{"author":"Jean Rhys","title":"Wide Sargasso Sea","id":"D0UY9kmrV6HbqlIMspVwn"},{"author":"Aldous Huxley","title":"Brave New World","id":"rK2ks0GbZBDQPns-ZDEyW"},{"author":"Dodie Smith","title":"I Capture The Castle","id":"flTB4dqKfg1PWcUI6KtH2"},{"author":"Charlotte Bronte","title":"Jane Eyre","id":"3x-S6EsNUTZ5l_sESamF_"},{"author":"Fyodor Dostoevsky","title":"Crime and Punishment","id":"ntH3G63fMVKUud6rRhDbY"},{"author":"Donna Tartt","title":"The Secret History","id":"ubrxbS1-7NEr_lml6I8Q3"},{"author":"Jack London","title":"The Call of the Wild","id":"friqBlVlEY3eg2cpkgUET"},{"author":"John Wyndham","title":"The Chrysalids","id":"wRMQGG1QYaeVXXP_ghl-x"},{"author":"Jane Austen","title":"Persuasion","id":"YoMqTM9PhAfctMBqSdz6P"},{"author":"Herman Melville","title":"Moby-Dick","id":"Kd0Oggfkf5AQPGBqpw_iE"},{"author":"C.S. Lewis","title":"The Lion, the Witch and the Wardrobe","id":"-jD0Ujt-r54xbKZ_7Jv59"},{"author":"Virginia Woolf","title":"To the Lighthouse","id":"1TJQYcP6_hwm2syHUH8Dv"},{"author":"Elizabeth Bowen","title":"The Death of the Heart","id":"dl1qbyM0cHdmYUHKhTyZk"},{"author":"Thomas Hardy","title":"Tess of the d'Urbervilles","id":"_i6SLfaMpXRuhVqEH5Jhp"},{"author":"Mary Shelley","title":"Frankenstein","id":"ZPL-swiUogF-_gdabf9qv"},{"author":"Mikhail Bulgakov","title":"The Master and Margarita","id":"x0pw07n3o2KljHZM11isw"},{"author":"L. P. Hartley","title":"The Go-Between","id":"l0jHUSb4bY64k-l9Qed5Z"},{"author":"Ken Kesey","title":"One Flew Over the Cuckoo's Nest","id":"SCKsZTWD2QMsNomUie_Vf"},{"author":"George Orwell","title":"Nineteen Eighty-Four","id":"JscV73l2tSdm5W4kZSvZn"},{"author":"Thomas Mann","title":"Buddenbrooks","id":"f0XqwYfsWJ-w9J18b5FCD"},{"author":"John Steinbeck","title":"The Grapes of Wrath","id":"OnXfkmQEAL7sSQ3PgSV9z"},{"author":"Toni Morrison","title":"Beloved","id":"n3_aZgBlQkphqPTvmJGr6"},{"author":"P. G. Wodehouse","title":"The Code of the Woosters","id":"TzD6k5flXf8HMdfgSacMT"},{"author":"Bram Stoker","title":"Dracula","id":"_WPS6E_6uXVKWX0r2Sop6"},{"author":"J. R. R. Tolkien","title":"The Lord of the Rings","id":"bIzyksKmB0plzGwWI6h7l"},{"author":"Mark Twain","title":"The Adventures of Huckleberry Finn","id":"ctQZfUT_tsujBCdYkv4HA"},{"author":"Charles Dickens","title":"Great Expectations","id":"ULj9NAatfo8tCCe39YZTY"},{"author":"Joseph Heller","title":"Catch-22","id":"bOOUBZK7oFVDRrevxApvN"},{"author":"Edith Wharton","title":"The Age of Innocence","id":"ZJ8y0y-BbnaH5A9TulxgN"},{"author":"Chinua Achebe","title":"Things Fall Apart","id":"eahxg8sFYsudKEl9hocJv"},{"author":"George Eliot","title":"Middlemarch","id":"TLNUskf7TspVe3AOEV4nX"},{"author":"Salman Rushdie","title":"Midnight's Children","id":"0_DeHTlQpW4ffy-liu2R-"},{"author":"Homer","title":"The Iliad","id":"D9cyf2yCAwhnASsxGxtTd"},{"author":"William Makepeace Thackeray","title":"Vanity Fair","id":"YmXxLcLMYmuFkp39Q1aAa"},{"author":"Evelyn Waugh","title":"Brideshead Revisited","id":"p3D_ZtFdhT2Eytv7swOAZ"},{"author":"J.D. Salinger","title":"The Catcher in the Rye","id":"3Sf-5_lsdGVeiWJeSZZQI"},{"author":"Lewis Carroll","title":"Alice’s Adventures in Wonderland","id":"TJJ6J8OHF5PRaiHLEcPdq"},{"author":"George Eliot","title":"The Mill on the Floss","id":"F6S5twxijUt7cSvuoSeKH"},{"author":"Anthony Trollope","title":"Barchester Towers","id":"0jYVd6dhiSF1tJYuIU8az"},{"author":"James Baldwin","title":"Another Country","id":"xRjGwu2vOQObLqbFccnw_"},{"author":"Victor Hugo","title":"Les Miserables","id":"GR24l64YVjFagi-SB1Y-H"},{"author":"Roald Dahl","title":"Charlie and the Chocolate Factory","id":"CAoAoALD3T8wxX0Eevabi"},{"author":"S. E. Hinton","title":"The Outsiders","id":"XYhNMkKTKsh9aNGh24fvZ"},{"author":"Alexandre Dumas","title":"The Count of Monte Cristo","id":"Igcm-Wxq2Uf8vKjBr-D7j"},{"author":"James Joyce","title":"Ulysses","id":"GiianKDQPQVTIaFoFhy6H"},{"author":"John Steinbeck","title":"East of Eden","id":"belUus-Sta74zWfjTiuMW"},{"author":"Fyodor Dostoyevsky","title":"The Brothers Karamazov","id":"wp9JOJ0B8lKmxG0siRuR4"},{"author":"Vladimir Nabokov","title":"Lolita","id":"tvnoXyLsd-PtVmiwZLnM8"},{"author":"Frances Hodgson Burnett","title":"The Secret Garden","id":"VZyJI95JMwkj4rJOJbzzn"},{"author":"Evelyn Waugh","title":"Scoop","id":"QYgFDNe1S0x5V_ub-Vc-S"},{"author":"Charles Dickens","title":"A Tale of Two Cities","id":"G0FUeqOiLuNnBNEr4XPD2"},{"author":"George Grossmith and Weedon Grossmith","title":"Diary of a Nobody","id":"PLi0tMjdAZI54P3U02B2N"},{"author":"Leo Tolstoy","title":"Anna Karenina","id":"E0OlPZ9F8Z3rsEmGihW-0"},{"author":"Alessandro Manzoni","title":"The Betrothed","id":"hPHRkfbcMUeJUejXy7spa"},{"author":"Virginia Woolf","title":"Orlando","id":"FSzptVHC-ICRl0tlPhS-O"},{"author":"Ayn Rand","title":"Atlas Shrugged","id":"CdzIlNo9jp5CDAP5BEwLi"},{"author":"H. G. Wells","title":"The Time Machine","id":"dQn4oEs0hqgfuaFR13S-o"},{"author":"Sun-Tzu","title":"The Art of War","id":"LZwoJLEtLv4Dx2QnUBvwM"},{"author":"John Galsworthy","title":"The Forsyte Saga","id":"p9hOPd4gC7PKX9bbp8JVZ"},{"author":"John Steinbeck","title":"Travels with Charley","id":"c3LtQi5_p-XSF2JSfPOjq"},{"author":"Henry Miller","title":"Tropic of Cancer","id":"iFILNdFzltGXugvwpUjSS"},{"author":"D. H. Lawrence","title":"Women in Love","id":"gYf7mAVCM_SX5e3NDwc9y"},{"author":"Paul Scott","title":"Staying On","id":"gZYOkRz4APlcDGNH5onYD"},{"author":"Kenneth Grahame","title":"The Wind in the Willows","id":"epTCvsskVjm3vnomZCPRw"},{"author":"Willa Cather","title":"My Ántonia","id":"wWoBKiKEQ6KpwigH2RtMQ"},{"author":"Emily Brontë","title":"Wuthering Heights","id":"8Feh8HOHmfFZXwhkclUmj"},{"author":"Patrick Süskind","title":"Perfume","id":"JJntMbxqiKvuryEO82VAX"},{"author":"Leo Tolstoy","title":"War and Peace","id":"CPfDnuxwDYeLvzqLPJzXJ"},{"author":"Somerset Maugham","title":"Of Human Bondage","id":"h4IW8mQUmLTJ9uyfVe2qe"},{"author":"Charles Dickens","title":"Bleak House","id":"NPkSH3PieOiq_gE0svlxB"},{"author":"Honoré de Balzac","title":"Lost Illusions","id":"0Ckpg5CMzAYIUbCjWZXPt"},{"author":"Kurt Vonnegut","title":"Breakfast of Champions","id":"Lydqp4eMEkYL3YVkg0krr"},{"author":"Charles Dickens","title":"A Christmas Carol","id":"ApOCi4LPkvoN2R47C1frw"},{"author":"George Eliot","title":"Silas Marner","id":"5CUwpkfRyLjTBBmJHc0Ic"},{"author":"Virginia Woolf","title":"Mrs Dalloway","id":"9Pdh2b7of93bT-Xp1egBB"},{"author":"Louisa May Alcott","title":"Little Women","id":"095_BrLfJD-pI2nOtqJII"},{"author":"Iris Murdoch","title":"The Sea, The Sea","id":"5V4JjZvcqWhiLTdpYjc5r"},{"author":"Mario Puzo","title":"The Godfather","id":"cK1YXvMZ4xRZVFyQDKcG3"},{"author":"Franz Kafka","title":"The Castle","id":"bV5hrXcPzSfPhLPITPlj7"},{"author":"Robert Graves","title":"I, Claudius","id":"2FFaA72V-Pp74A6mZajR7"},{"author":"J.M. Barrie","title":"Peter Pan","id":"6vwOgrhQTp60ISU-KIxoQ"},{"author":"John Kennedy Toole","title":"A Confederacy of Dunces","id":"zZwqEBfR72Ht_Uwa25blx"},{"author":"W. Somerset Maugham","title":"The Razor's Edge","id":"uL-eIpi0xf11BDmpxfxYQ"},{"author":"Flora Thompson","title":"Lark Rise to Candleford","id":"wISh6hRf-rIOXzGV9pReU"},{"author":"Thomas Hardy","title":"The Return of the Native","id":"ouX9cTm5gF36zX95SfOaE"},{"author":"James Joyce","title":"A Portrait of the Artist as a Young Man","id":"dX6B1SNtZH_Kij9ZdQ3cx"},{"author":"Joseph Conrad","title":"Heart of Darkness","id":"uQk4tRerBAtFtZwh-Xyx3"},{"author":"Elizabeth Gaskell","title":"North and South","id":"8bRGCx_5Pk3i4-RNXlley"},{"author":"Margaret Atwood","title":"The Handmaid's Tale","id":"E0tJsPHR6JnnoQ9UKtKHE"},{"author":"Irene Nemirovsky","title":"Suite Francaise","id":"0lq5lUjV7A0SMvUF-ucmv"},{"author":"Alexander Solzhenitsyn","title":"One Day in the Life of Ivan Denisovich","id":"3Qik1V1BoZZDyPphzedzb"},{"author":"Jonathan Coe","title":"What A Carve Up!","id":"UhNcCOU_TzUDbTOvxzUPU"},{"author":"Robert Pirsig","title":"Zen and the Art of Motorcycle Maintenance","id":"Alpfu_s-Ee8L6G1s7-WD2"},{"author":"Fyodor Dostoyevsky","title":"White Nights","id":"Lr3KmI-pOxer7rSsF8MhE"},{"author":"Charles Dickens","title":"Hard Times","id":"OrxuKkQoEgg2cSDQcyyPc"}]`) as Book[])
.forEach(book => db.set(book.id, book));
function delay (ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
function randomInt (min = 0, max = 1): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// Simulated db methods
const booksDb = {
async getOne (id: string): Promise<Book | undefined> {
return (await this.getMany([id]))[0];
},
async getMany (ids: string[]): Promise<(Book | undefined)[]> {
await delay(randomInt(50, 500));
return ids.map(id => {
const book = db.get(id);
// Simulate getting a copy every time
return book ? {...book} : undefined;
});
},
};
// Recoil state:
// Cached results
const booksCache = new Map<string, Book>();
// Just for this demo, maintain a query count
let dbQueryCount = 0;
// Inspired by effector, I prefix recoil-related variables with $ to simplify naming
const $book = atomFamily<Book | undefined, string>({
key: 'book',
default: async (id) => {
// Return from cache, querying db only if unavailable
if (!booksCache.has(id)) {
dbQueryCount += 1;
const book = await booksDb.getOne(id);
if (book) booksCache.set(id, book);
}
return booksCache.get(id);
},
});
const $books = atomFamily<(Book | undefined)[], string[]>({
key: 'books',
default: async (ids) => {
const books: (Book | undefined)[] = [];
const available: [index: number, id: string][] = [];
const unavailable: [index: number, id: string][] = [];
// Split query into collections of available in cache or not
for (const [index, id] of ids.entries()) {
const isAvailable = booksCache.has(id);
(isAvailable ? available : unavailable).push([index, id]);
}
// Get cached results
for (const [index, id] of available) {
books[index] = booksCache.get(id);
}
// Query the remaining with a single network request
dbQueryCount += 1;
const booksFromDb = await booksDb.getMany(unavailable.map(([, id]) => id));
// Update cache and finalize
for (const [index, id] of unavailable) {
const book = booksFromDb[index];
if (book) booksCache.set(id, book);
books[index] = booksCache.get(id);
}
return books;
},
});
// Components:
function BookComponent ({book}: { book: Book | undefined }): ReactElement {
if (!book) return (<div>Book is not availble</div>);
return (
<div>
<em>{book.title}</em> by <span>{book.author}</span>
</div>
);
}
function BookFromId ({id}: { id: string }): ReactElement {
const book = useRecoilValue($book(id));
return <BookComponent {...{book}} />;
}
function BookCollection ({ids}: { ids: string[]; }): ReactElement {
// To see these loaded individually, uncomment the following lines:
// return (<div>{ids.map((id, index) => (
// <BookFromId {...{id, key: `${index}-${id}`}} />
// ))}</div>);
const books = useRecoilValue($books(ids));
return (<div>{books.map((book, index) => (
<BookComponent {...{book, key: `${index}-${book?.id}`}} />
))}</div>);
}
function LoadingDiv ({children}: { children?: ReactNode }): ReactElement {
return (<div>{children}</div>);
}
const collections: [title: string, ids: string[]][] = [
['Titles starting with A', ['ApOCi4LPkvoN2R47C1frw', 'zZwqEBfR72Ht_Uwa25blx', 'dX6B1SNtZH_Kij9ZdQ3cx', 'G0FUeqOiLuNnBNEr4XPD2', 'TJJ6J8OHF5PRaiHLEcPdq', 'E0OlPZ9F8Z3rsEmGihW-0', 'xRjGwu2vOQObLqbFccnw_', 'CdzIlNo9jp5CDAP5BEwLi']],
['Titles starting with B', ['0jYVd6dhiSF1tJYuIU8az', 'n3_aZgBlQkphqPTvmJGr6', 'NPkSH3PieOiq_gE0svlxB', 'rK2ks0GbZBDQPns-ZDEyW', 'Lydqp4eMEkYL3YVkg0krr', 'p3D_ZtFdhT2Eytv7swOAZ', 'f0XqwYfsWJ-w9J18b5FCD']],
['Titles starting with C', ['bOOUBZK7oFVDRrevxApvN', 'CAoAoALD3T8wxX0Eevabi', 'ntH3G63fMVKUud6rRhDbY']],
];
type OrPromise<T> = T | Promise<T>;
function useLazyValue <T>(initialValue: T, producer: () => OrPromise<T>): T {
const [value, setValue] = useState(initialValue);
const updateValue = async () => {
const result = await producer();
if (value !== result) setValue(result);
};
useEffect(() => void updateValue());
return value;
}
function App (): ReactElement {
const [collectionIndex, setCollectionIndex] = useState(0);
const collectionIds = collections[collectionIndex]![1];
const queryCount = useLazyValue(0, () => dbQueryCount);
const booksLoading = <LoadingDiv>The collection is loading...</LoadingDiv>;
return (
<div style={{
display: 'flex',
flexDirection: 'column',
gap: '0.5rem',
fontFamily: 'sans-serif',
}}>
<h1>Recoil book cache</h1>
<div>Query count: {queryCount}</div>
<label>
<div>Select a collection:</div>
<select
onChange={ev => setCollectionIndex(Number(ev.target.value))}
value={collectionIndex}
>{collections.map(([title], index) => (
<option key={`${index}-${title}`} value={index}>{title}</option>
))}</select>
</label>
<Suspense fallback={booksLoading}>
<BookCollection ids={collectionIds} />
</Suspense>
</div>
);
}
function AppRoot (): ReactElement {
return (
<RecoilRoot>
<App />
</RecoilRoot>
);
}
ReactDOM.render(<AppRoot />, document.getElementById('root'));
</script>

React useState changes all elements in an array in array of objects

I have 2 states product and variations I call an API and set the values of both state to the API response.
I want the product state to stay as it is and not update
const [product, setProduct] = useState({} as any);
const [variations, setVariations] = useState([] as any);
useEffect(() => {
const getProduct = async () => {
const data = await axios.get("/products?id=4533843820679");
console.log(data);
setProduct(data.data);
// #ts-ignore
setVariations([data.data]);
};
getProduct();
}, []);
In return I map the variations array and return inputs for title, and price and a button to add variations. Adding variations will add another product to variations array. So it just pushes product to variations.
Then I have inputs for title in variation and prices in variation.variants. The problem is with onChange.
When I change the price of one element in variants it changes for all and also changes it for PRODUCT state.
The code can be found here: https://codesandbox.io/s/smoosh-firefly-6n747?file=/src/App.js
Add variations, change prices add another variations and you'll see all issues I'm facing.
It is because of this:
variant.price = e.target.value; // same issue with title
the variant object reference is shared among variations and you are modifying it directly. It is shared because you you made a shallow copy of a variation using ... when adding it.
Here is the solution:
You should update the specific variant object in immutable way (in react you should always update state in immutable way). For that you need to use this as onChange for price:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
variants: x.variants.map((y) => {
if (y.id === variant.id) {
return {
...y,
price: e.target.value
};
}
return y;
})
};
}
return x;
});
setVariations(updated);
}
}
This for onChange for title:
onChange = {
(e) => {
let updated = variations.map((x) => {
if (x.id === variation.id) {
return {
...x,
title: e.target.value
};
}
return x;
});
setVariations(updated);
}
}
NOTE but ids of variations must be different. For testing purposes you can use this as click handler when adding a new variation:
onClick = {
() => {
setVariations((prev) => [...prev, {
...product,
id: Math.floor(Math.random() * 1000) // for testing
}]);
}
}
First, you are not pushing the product to variations. You are overwriting it.
To push a value to array with useState,
setVariations([...variations, product])
But, if you change the product object, variations also gonna be change because it's the same object. (Maybe, react not gonna re-render it but trust me, it is changed.) If you want to keep it same you need to create new object.
So,
setProduct(data.data);
setVariations([...variations, {...data.data}]);
Now, you can change product. variations not gonna change.
This was because you did a shallow copy of an object.
Try to do like this:
setVariations([...variations, data.data,]);

Best way to join 2 API data sources into 1 react table?

I need data from 2 different APIs, both support pagination.
How can I display the joined data in a table in a performant way?
I need to join on their id, but one API returns less data than the other, so I cannot simply match 1 by 1. I need to implement a filter.
Is the only way to brute force map data source A to B?
If the data comes from two different APIs and you are making to separate requests you have a number of options. My personal preference is to have state in your controller in which you map each response by id and then you can select the additional data by id:
import React, { useState, useEffect } from 'react';
import { keyBy } from 'lodash';
function TableComponent(props) {
// Destructure your props...I'm assuming you pass some id into fetch data
const { id } = props;
// State
const [tableData, setTableData] = useState([]);
// Load data when id changes
useEffect(() => {
fetchData()
}, [id]);
async function fetchData() {
// Get your data from each source
const apiData_A = await fetchDataFromAPI_A(id);
const apiData_B = await fetchDataFromAPI_B(id);
// Key each data set by result ids
const resultsMappedById_A = keyBy(apiData_A, 'id');
const resultsMappedById_B = keyBy(apiData_B, 'id');
// Combine data into a single set
// this assumes your getting same results from each api
const combinedDataSet = Object.keys(resultsMappedById_A)
.reduce((acc, key) => {
// Destructure results together, merging objects
acc.push({
...resultsMappedById_A[key],
...resultsMappedById_B[key]
});
return acc;
}, []);
setTableData(combinedDataSet);
}
async function fetchDataFromAPI_A(id) {
// Fetch your data and return results
}
async function fetchDataFromAPI_A(id) {
// Fetch your data and return results
}
function renderTableRow(data) {
return (
<tr>
<td>{data.id}</td>
<td>{data.apiAProp}</td>
<td>{data.apiBProp}</td>
</tr>
);
}
return (
<table>
{ tableDataSet.map(renderTableRow) }
</table>
);
}
Note, there are probably more efficient ways to do this depending on how you're fetching data and what the responses hold, but the concept provided here should do the trick assuming my assumptions are correct based on the information you have provided.

updating a single object key value inside array of objects react redux state

I am trying to update an object and its key value inside an array using react redux but as I am new to react and redux, so I am not finding a good way to do this and also the value, is not updating to.
Here is my action
export const addIngredientToMenuItemCartA = (menu_item_id,timestamp,ingrediant,ingrediant_type,selectedMenuItemIngrediantType)
=> async dispatch => {
dispatch({
type: ADD_INGREDIENT_TO_MENU_ITEM_CART,
payload: {
menu_item_id,
timestamp,
ingrediant,
ingrediant_type,
ingrediant_category_type_blue: selectedMenuItemIngrediantType
}
});
};
Here is my reducer
export default function(state=[],action){
case ADD_INGREDIENT_TO_MENU_ITEM_CART:
let menu_item_id = action.payload.menu_item_id;
let ingrediant = action.payload.ingrediant;
let timestamp = action.payload.timestamp;
let items1 = state.slice();
const itemIndexi1 = items1.findIndex(item => item.menu_item_id === menu_item_id);
if(true){
items1[itemIndexi1].ingrediantTotal = ingrediant.price;
}
items1[itemIndexi1].ingrediants.push(ingrediant);
return items1;
default:
return state;
}
I have an array of the cart which has objects inside it and I want to find that specific objects and then update them but if I update them in the reducer then the values are not being changed in the store.
It seems that you are mutating the objects. There is a simple pattern to loop over the list and handle only the relevant item while creating new objects without mutations.
In your case, your code could look something like this:
case ADD_INGREDIENT_TO_MENU_ITEM_CART: {
const { menu_item_id, ingrediant } = action.payload;
const nextState = state.map(item => {
if (item.menu_item_id !== menu_item_id) {
// not our item, return it as is
return item;
}
// this is our relevant item, return a new copy of it with modified fields
return {
...item,
ingrediantTotal: ingrediant.price,
ingrediants: [
...item.ingrediants,
ingrediant
]
}
});
return nextState;
}
Keep in mind, Objects and Arrays are mutable so we can use the spread syntax (...) or .slice etc.

Object assigning in JavaScript

There is an object product which has object manufacturer as its field. After fetching manufacturers from my server, I reassign product's field manufacturer with a new data (for example, fetched manufacturer has additional object avatar as its field).
async componentDidMount() {
strapi.setToken(this.state.user.jwt);
const products = await strapi.getEntries('products');
products.forEach(async product => {
const manufacturer = await strapi.getEntry('manufacturers', product.manufacturer.id); //fetched manufacturers has additional field "avatar"
Object.assign(product.manufacturer, manufacturer);
});
console.log(products); // product.manufacturer.avatar is not null
this.setState({
products
});
Then I'm trying to display avatar in React render().
render() {
if (!this.state.products) return null;
return(
{this.state.products ? this.state.products.map(product => (
// and it says that avatar is null
<img src={product.manufacturer.avatar.url} />
// displays manufacturer with avatar object
{console.log(product.manufacturer)}
// says null!
{console.log(product.manufacturer.avatar)}
))}
)
}
Also when I check state with React Developer Tools, product's field manufacturer has object avatar and it isn't null.
UPDATE:
Thanks to Sergey Suslov
strapi.setToken(user.jwt);
const fetchedProducts = await strapi.getEntries('products');
const promices = fetchedProducts.map(async fetchedProduct => {
const manufacturer = await strapi.getEntry('manufacturers', fetchedProduct.manufacturer.id);
const product = { ...fetchedProduct, manufacturer };
return product;
});
Promise.all(promices).then(products =>
this.setState({
products
})
);
You are assigning with the assign method that does not mutate source object, it returns new object as a result, check this https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign.
You need to do semthing like this:
product = {
...product,
manufacturer: your assign statment
}
Main reason is that your callback function in foreach is asynchronous, js does not wait for all foreach function calls to completed, it keep on running, couse async word, try to use Promise for this, or better try to use actions for async requests, thank librariy or sagas.
It's because at first render, you do not have the avatar value. These values are fetched after the first render (componentDidMount).
You need to add a test to take that first render into account.
Also, the reason why your console.log is not consistent, is because you are overwriting the value of product, to be more immutable, you should .map instead of forEach, and return a copy of your product instead of modifying the existing one.
How I would write it:
constructor(props){
super(props);
this.state = {
products: []
}
}
render() {
return this.state.products.map(product => {
const {
avatar = {}
} = product.manufacturer;
return (
<img src={avatar.url} />
);
});
}

Categories