Processing action-creatos with dependencies in React via redux-toolkit - reactjs

In a React + Redux frontend I'm currently experimenting with integrating HATEOAS into the overall process. The application initially starts without any knowledge other than the base URI the backend can be found at and the backend will add URIs to each resource the frontend is requesting.
The application itself currently is able to upload an archive file that contains files the backend should process based on some configuration the frontend is passing to the backend via multipart/form-data upload. Once the upload finished, the backend will create a new task for the upload and start processing the upload which results in some values being calculated that end up in a database where a controller in the backend is responsible for exposing various resources for certain files processed.
I currently use ReduxJS toolkit and slices combined with async thunks to manage and access the Redux store. The store itself has an own section dedicated for stuff returned by the backend API, i.e. the global links section, the page information and so forth and will store the result of each invokation with a slight remapping.
One of the challenges I faced here is that on initially requesting the component responsible for rendering all tasks, this component first needs to lookup the link for a predefined link-relation name in the backend API and on consecutive calls should reuse the available information. Here I came up with an action creator function like this:
type HalLinks = { [rel: string]: HalLink };
const requestLinks = async (uri: string, state: ApiState): Promise<[HalLinks, APILinkResponse | undefined]> => {
let links: APILinkResponse | undefined;
let rels: { [rel: string]: HalLink; };
if (!state.links || Object.keys(state.links).length === 0) {
console.debug(`Requesting links from ${uri}`);
const linkLookup = await axios(uri, requestConfig);
links = linkLookup.data;
if (links) {
rels = links._links;
} else {
throw new Error('Cannot resolve links in response');
}
} else {
links = undefined;
rels = state.links;
}
return [rels, links];
}
const lookupUriForRelName = (links: HalLinks, relName: string): HalLink | undefined => {
if (links) {
if (relName in links) {
return links[relName];
} else {
return undefined;
}
} else {
return undefined;
}
}
const requestResource = async (link: HalLink, pageOptions?: PageOptions, filterOptions?: FilterOptions) => {
let href: string;
if (link.templated) {
const templated: URITemplate = utpl(link.href);
href = fillTemplateParameters(templated, pageOptions, filterOptions);
} else {
href = link.href;
}
console.debug(`Attempting to request URI ${href}`)
const response = await axios.get(href, requestConfig);
return response.data;
}
const lookup = async <T> (state: ApiState, uri: string, relName: string, pageOptions?: PageOptions, filterOptions?: FilterOptions): Promise<[T, APILinkResponse | undefined]> => {
const [rels, links] = await requestLinks(uri, state);
const link: HalLink | undefined = lookupUriForRelName(rels, relName);
if (link) {
const data = await requestResource(link, pageOptions, filterOptions);
return [data, links];
} else {
throw new Error('No link relation for relation name ' + relName + ' found');
}
}
export const requestTasksAsync = createAsyncThunk<[APITasksResponse, APILinkResponse | undefined], { uri: string; pageOptions?: PageOptions; filterOptions?: FilterOptions; }>(
'api/tasks',
async ({ uri, pageOptions, filterOptions }: { uri: string, pageOptions?: PageOptions, filterOptions?: FilterOptions }, { getState }) => {
const state: ApiState = getState() as ApiState;
const TASK_REL = 'http://acme.com/rel/tasks'; // RFC 8288 compliant link relation extension according to section 2.1.2
return await lookup<APITasksResponse>(state, uri, TASK_REL, pageOptions, filterOptions);
}
);
In the code above I basically lookup up the links returned from the resource identified by the initial URI in case they are not present in the Redux store or in the state yet or are to old. In case we collected the links before this step is skipped and instead the information available within the state/store is reused (all happening in the requestLinks(...) function). Once the links are available we can finally request the resource that serves the tasks information we want to obtain.
The reducer for the action creator above looks like this:
export const apiSlice = createSlice({
name: 'api',
initialState,
reducers: {
...
},
extraReducers: (builder) => {
builder
...
.addCase(requestTasksAsync.fulfilled, (state, action) => {
const parts: [APITasksResponse, APILinkResponse | undefined] = action.payload;
const tasks: APITasksResponse = parts[0];
const linksResponse: APILinkResponse | undefined = parts[1];
// update any link information received in preceeding calls ot
// the performed actin
if (linksResponse) {
processsLinks(linksResponse._links, state);
}
processsLinks(tasks._links, state);
processPage(tasks.page, state);
processTasks(tasks._embedded.tasks, state);
state.status = StateTypes.SUCCESS;
})
...
}
});
where basically the two HTTP responses are taken and processed. In case we already retrieved those URIs before we don't have to lookup those again and as such the links response is undefined which we simply filter out with an if-statement.
The respective process functions are just helper functions to reduce the code in the reducer. I.e. the processTasks function just adds the task for the given taskId to the record present in the state:
const processTasks = (tasks: TaskType[], state: ApiState) => {
for (let i = 0; i < tasks.length; i++) {
let task: TaskType = tasks[i];
state.tasks[task.taskId] = task;
}
}
while links are separated into global URIs and component ones based on whether a custom link relation is used or one specified by IANA (i.e. up, next, prev, ...)
const processsLinks = (links: { [rel: string]: HalLink }, state: ApiState) => {
if (links) {
Object.keys(links).forEach(rel => {
if (rel.startsWith('http')) {
if (!state.links[rel]) {
console.debug(`rel ${rel} not yet present in state.links. Adding it with URI ${links[rel]}`);
state.links[rel] = links[rel];
} else {
console.debug(`rel ${rel} already present in state with value ${state.links[rel]}. Not going to add value ${links[rel]}`);
}
} else {
if (state.current) {
console.debug(`Updateing rel ${rel} for current component to point to URI ${links[rel]}`);
state.current.links[rel] = links[rel];
} else {
state.current = { links: { [rel]: links[rel] } };
console.debug(`Created new object for current component and assigned it the link for relation ${rel} with uri ${links[rel]}`);
}
}
});
}
}
Within the TaskOverview component the action is basically dispatched with the code below:
const [pageOptions, setPageOptions] = useState<PageOptions | undefined>();
const [filterOptions, setFilterOptions] = useState<TaskFilterOptions | undefined>()
const state: StateTypes = useAppSelector(selectState);
const current = useAppSelector(selectCurrent);
const tasks: Record<string, TaskType> = useAppSelector(selectTasks);
const dispatch = useAppDispatch();
useEffect(() => {
...
// request new tasks if we either have not tasks yet or options changed and we
// are not currently loading them
if (StateTypes.IDLE === state) {
// lookup tasks
dispatch(requestTasksAsync({uri: "http://localhost:8080/api", pageOptions: pageOptions, filterOptions: filterOptions}));
}
...
}, [dispatch, state, tasks, pageOptions, filterOptions])
The code above works. I'm able to lookup the URI based on the link relation and get the correct URI to retrieve the data exposed by the tasks resource and store those information into the Redux store. However, this all feels extremely clunky as I have to return two response objects from the action creation as I'm neither allowed to issue a dispatch from within a non-functional component nor alter the state within an action itself.
Ideally I'd love to dispatch actions in some way from within an async thunk and have a callback that informs me once the data is available in the store but I guess this is not possible. As I'm still fairly new to React/Redux I wonder if there is somehow a better approach available to trigger actions based on certain dependencies and in the absence of those load those dependencies before? I'm aware though that I could simply split the actions up into separated ones and then do the invocations within the respective component, though it feels like it will a) drag some of the state management logic the slice is responsible for into the respective component and b) duplicate some code.

You can totally dispatch from within an asyncThunk.
export const requestTasksAsync = createAsyncThunk<[APITasksResponse, APILinkResponse | undefined], { uri: string; pageOptions?: PageOptions; filterOptions?: FilterOptions; }, { state: ApiState }>(
'api/tasks',
async ({ uri, pageOptions, filterOptions }, { getState, dispatch }) => {
const state: ApiState = getState();
const TASK_REL = 'http://acme.com/rel/tasks'; // RFC 8288 compliant link relation extension according to section 2.1.2
dispatch(whatever())
return await lookup<APITasksResponse>(state, uri, TASK_REL, pageOptions, filterOptions);
}
);
Also, you don't need to repeat the arg type both in the generic and the payload generator function.
In your case, either use the generic (and then also move the ApiState type up into the generic), or skip the generic definition at the top and type everything inline.

Related

Infinite scroll with RTK (redux-toolkit)

I'm struggling to implement full support of infinite scroll with cursor pagination, adding new elements and removing. I have used great example from github discussion https://github.com/reduxjs/redux-toolkit/discussions/1163#discussioncomment-876186 with few adjustments based on my requirements. Here is my implementation of useInfiniteQuery.
export function useInfiniteQuery<
ListOptions,
ResourceType,
ResultType extends IList<ResourceType> = IList<ResourceType>,
Endpoint extends QueryHooks<
QueryDefinition<any, any, any, ResultType, any>
> = QueryHooks<QueryDefinition<any, any, any, ResultType, any>>,
>(
endpoint: Endpoint,
{
options,
getNextPageParam,
select = defaultSelect,
inverseAppend = false,
}: UseInfiniteQueryOptions<ListOptions, ResultType, ResourceType>,
) {
const nextPageRef = useRef<string | undefined>(undefined);
const resetRef = useRef(true);
const [pages, setPages] = useState<ResourceType[] | undefined>(undefined);
const [trigger, result] = endpoint.useLazyQuery();
const next = useCallback(() => {
if (nextPageRef.current !== undefined) {
trigger(
{ options: { ...options, page_after: nextPageRef.current } },
true,
);
}
}, [trigger, options]);
useEffect(() => {
resetRef.current = true;
trigger({ options }, true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, Object.values(options));
useEffect(() => {
if (!result.isSuccess) {
return;
}
nextPageRef.current = getNextPageParam(result.data);
if (resetRef.current) {
resetRef.current = false;
setPages(select(result.data));
} else {
setPages((pages) =>
inverseAppend
? select(result.data).concat(pages ?? [])
: (pages ?? []).concat(select(result.data)),
);
}
}, [result.data, inverseAppend, getNextPageParam, select, result.isSuccess]);
return {
...result,
data: pages,
isLoading: result.isFetching && pages === undefined,
hasMore: nextPageRef.current !== undefined,
next,
};
}
This example works great with pagination, but the problem when I'm trying to add new elements. I can't distinguish are new elements arrived from new pagination batch or when I called mutation and invalidated tag (in this case RTK refetch current subscriptions and update result.data which I'm listening in second useEffect).
I need to somehow to identify when I need to append new arrived data (in case of next pagination) or replace fully (in case when mutation was called and needs to reset cursor and scroll to top/bottom).
My calling mutation and fetching the data placed in different components. I tried to use fixedCacheKey to listening when mutation was called and needs to reset data, but I very quickly faced with a lot of duplication and problem when I need to reuse mutation in different places, but keep the same fixed cache key.
Someone has an idea how to accomplish it? Perhaps, I need to take another direction with useInfiniteQuery implementation or provide some fixes to current implementation. But I have no idea to handle this situation. Thanks!

How do you access query arguments in getSelectors() when using createEntityAdapter with RTK Query

I've been following along the REDUX essentials guide and I'm at part 8, combining RTK Query with the createEntityAdapter. I'm using the guide to implement it in a personal project where my getUni endpoint has an argument named country, as you can see from the code snippet below.
I'm wondering is there anyway to access the country argument value from the state in universityAdaptor.getSelector(state => ) at the bottom of the snippet, as the query key name keeps changing.
import {
createEntityAdapter,
createSelector,
nanoid
} from "#reduxjs/toolkit";
import {
apiSlice
} from "../api/apiSlice";
const universityAdapter = createEntityAdapter({})
const initialState = universityAdapter.getInitialState();
export const extendedApiSlice = apiSlice.injectEndpoints({
endpoints: builder => ({
getUni: builder.query({
query: country => ({
url: `http://universities.hipolabs.com/search?country=${country}`,
}),
transformResponse: responseData => {
let resConvert = responseData.slice()
.sort((a, b) => a.name.localeCompare(b.name))
.map(each => {
return { ...each,
id: nanoid()
}
});
return universityAdapter.setAll(initialState, resConvert)
}
})
})
});
export const {
useGetUniQuery
} = extendedApiSlice;
export const {
selectAll: getAllUniversity
} = universityAdapter.getSelectors(state => {
return Object.keys({ ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }).length === 0
? initialState : { ...state.api.queries[<DYNAMIC_QUERY_NAME>]data }
})
UPDATE: I got it working with a turnery operator due to the multiple redux Actions created when RTK Query handles fetching. Wondering if this is best practice as I still haven't figured out how to access the country argument.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
return !Object.values(state.api.queries)[0]
? initialState : Object.values(state.api.queries)[0].status !== 'fulfilled'
? initialState : Object.values(state.api.queries)[0].data
})
I wrote that "Essentials" tutorial :)
I'm actually a bit confused what your question is - can you clarify what specifically you're trying to do?
That said, I'll try to offer some hopefully relevant info.
First, you don't need to manually call someEndpoint.select() most of the time - instead, call const { data } = useGetThingQuery("someArg"), and RTKQ will fetch and return it. You only need to call someEndpoint.select() if you're manually constructing a selector for use elsewhere.
Second, if you are manually trying to construct a selector, keep in mind that the point of someEndpoint.select() is to construct "a selector that gives you back the entire cache entry for that cache key". What you usually want from that cache entry is just the received value, which is stored as cacheEntry.data, and in this case that will contain the normalized { ids : [], entities: {} } lookup table you returned from transformResponse().
Notionally, you might be able to do something like this:
const selectNormalizedPokemonData = someApi.endpoints.getAllPokemon.select();
// These selectors expect the entity state as an arg,
// not the entire Redux root state:
// https://redux-toolkit.js.org/api/createEntityAdapter#selector-functions
const localizedPokemonSelectors = pokemonAdapter.getSelectors();
const selectPokemonEntryById = createSelector(
selectNormalizedPokemonData ,
(state, pokemonId) => pokemonId,
(pokemonData, pokemonId) => {
return localizedPokemonSelectors.selectById(pokemonData, pokemonId);
}
)
Some more info that may help see what's happening with the code in the Essentials tutorial, background - getLists endpoint takes 1 parameter, select in the service:
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId) : [];
};
And my selector in the slice:
export const selectAllLists = createSelector(getListsResult, (listsResult) => {
console.log('inside of selectAllLists selector = ', listsResult);
return listsResult.data;
// return useSelector(listsResult) ?? [];
});
Now this console logs listsResult as ƒ memoized() { function! Not something that can have .data property as tutorial suggests. Additionally return useSelector(listsResult) - makes it work, by executing the memoized function.
This is how far I got, but from what I understand, the code in the Essentials tutorial does not work as it is...
However going here https://codesandbox.io/s/distracted-chandrasekhar-r4mcn1?file=/src/features/users/usersSlice.js and adding same console log:
const selectUsersData = createSelector(selectUsersResult, (usersResult) => {
console.log("usersResult", usersResult);
return usersResult.data;
});
Shows it is not returning a memorised function, but an object with data on it instead.
Wonder if the difference happening because I have a parameter on my endpoint...
select returns a memoized curry function. Thus, call it with first with corresponding arg aka tribeId in your case and then with state. This will give you the result object back for corresponding chained selectors.
export const getListsResult = (state: RootState) => {
return state.tribeId ? extendedApi.endpoints.getLists.select(state.tribeId)(state) : [];
};
The intention of the getUni endpoint was to produce an array of university data. To implement the .getSelector function to retrieve that array, I looped over all query values, searching for a getUni query and ensuring they were fulfilled. The bottom turnery operator confirms the getUni endpoint was fired at least once otherwise, it returns the initialState value.
export const { selectAll: getAllUniversity } = universityAdapter
.getSelectors(state => {
let newObj = {};
for (const value of Object.values(state.api.queries)) {
if (value?.endpointName === 'getUni' && value?.status === 'fulfilled') {
newObj = value.data;
}
}
return !Object.values(newObj)[0] ? initialState : newObj;
})

How to define an action in a mobx store using mobx-react?

Have been following a few tutorials on youtube and have pretty much never seen anyone explicitly define an action that mutates the state they just throw in into the store. I have been doing the same and while it works a 100% it throws a warning on react native. Just wondering how you could define that something is an action and maybe if someone has a good way to separate the actions into a different file. Here is my store.
export function createCurrencyStore() {
return {
currencies: [
'AED',
'ARS',
'AUD',
],
selectedCurrencyFrom: 'USD',
selectedCurrencyTo: 'EUR',
loading: false,
error: null,
exchangeRate: null,
amount: 1,
fromFilterString: '',
fromFilteredCurrencies: [],
toFilterString: '',
toFilteredCurrencies: [],
setSelectedCurrencyFrom(currency) {
this.selectedCurrencyFrom = currency
},
setSelectedCurrencyTo(currency) {
this.selectedCurrencyTo = currency
},
async getExchangeRate() {
const conn = await fetch(
`https://api.exchangerate-api.com/v4/latest/${this.selectedCurrencyFrom}`
)
const res = await conn.json()
console.log(res)
this.exchangeRate = res.rates[this.selectedCurrencyTo]
},
setFromFilters(string) {
this.fromFilterString = string
if (this.fromFilterString !== '') {
this.fromFilteredCurrencies = this.currencies.filter((currency) =>
currency.toLowerCase().includes(string.toLowerCase())
)
} else {
this.fromFilteredCurrencies = []
}
},
setToFilters(string) {
this.toFilterString = string
if (this.toFilterString !== '') {
this.toFilteredCurrencies = this.currencies.filter((currency) =>
currency.toLowerCase().includes(string.toLowerCase())
)
} else {
this.toFilteredCurrencies = []
}
},
}
}
have pretty much never seen anyone explicitly define an action
Well, this is weird because it is a very common thing to only mutate state through actions to avoid unexpected mutations. In MobX6 actions are enforced by default, but you can disable warnings with configure method:
import { configure } from "mobx"
configure({
enforceActions: "never",
})
a good way to separate the actions into a different file
You don't really need to do it, unless it's a very specific case and you need to somehow reuse actions or something like that. Usually you keep actions and the state they modify together.
I am not quite sure what you are doing with result of createCurrencyStore, are you passing it to observable? Anyway, the best way to create stores in MobX6 is to use makeAutoObservable (or makeObservable if you need some fine tuning). So if you are not using classes then it will look like that:
import { makeAutoObservable } from "mobx"
function createDoubler(value) {
return makeAutoObservable({
value,
get double() {
return this.value * 2
},
increment() {
this.value++
}
})
}
That way every getter will become computed, every method will become action and all other values will be observables basically.
More info in the docs: https://mobx.js.org/observable-state.html
UPDATE:
Since your getExchangeRate function is async then you need to use runInAction inside, or handle result in separate action, or use some other way of handling async actions:
import { runInAction} from "mobx"
async getExchangeRate() {
const conn = await fetch(
`https://api.exchangerate-api.com/v4/latest/${this.selectedCurrencyFrom}`
)
const res = await conn.json()
runInAction(() => {
this.exchangeRate = res.rates[this.selectedCurrencyTo]
})
// or do it in separate function
this.handleExchangeRate(res.rates[this.selectedCurrencyTo])
},
More about async actions: https://mobx.js.org/actions.html#asynchronous-actions

GatsbyJS getting data from Restful API

I am new in both React and GatsbyJS. I am confused and could not make figuring out in a simple way to load data from third-party Restful API.
For example, I would like to fetch data from randomuser.me/API and then be able to use the data in pages.
Let’s say something like this :
import React from 'react'
import Link from 'gatsby-link'
class User extends React.Component {
constructor(){
super();
this.state = {
pictures:[],
};
}
componentDidMount(){
fetch('https://randomuser.me/api/?results=500')
.then(results=>{
return results.json();
})
.then(data=>{
let pictures = data.results.map((pic,i)=>{
return(
<div key={i} >
<img key={i} src={pic.picture.medium}/>
</div>
)
})
this.setState({pictures:pictures})
})
}
render() {
return (<div>{this.state.pictures}</div>)
}
}
export default User;
But I would like to get the help of GraphQL in order to filter & sort users and etc…..
Could you please help me to find the sample to how I can fetch data and insert them into GraphQL on gatsby-node.js?
If you want to use GraphQL to fetch your data, you have to create a sourceNode. The doc about creating a source plugin could help you.
Follow these steps to be able to query randomuser data with GraphQL in your Gatsby project.
1) Create nodes in gatsby-node.js
In your root project folder, add this code to gatsby-node.js:
const axios = require('axios');
const crypto = require('crypto');
exports.sourceNodes = async ({ actions }) => {
const { createNode } = actions;
// fetch raw data from the randomuser api
const fetchRandomUser = () => axios.get(`https://randomuser.me/api/?results=500`);
// await for results
const res = await fetchRandomUser();
// map into these results and create nodes
res.data.results.map((user, i) => {
// Create your node object
const userNode = {
// Required fields
id: `${i}`,
parent: `__SOURCE__`,
internal: {
type: `RandomUser`, // name of the graphQL query --> allRandomUser {}
// contentDigest will be added just after
// but it is required
},
children: [],
// Other fields that you want to query with graphQl
gender: user.gender,
name: {
title: user.name.title,
first: user.name.first,
last: user.name.last,
},
picture: {
large: user.picture.large,
medium: user.picture.medium,
thumbnail: user.picture.thumbnail,
}
// etc...
}
// Get content digest of node. (Required field)
const contentDigest = crypto
.createHash(`md5`)
.update(JSON.stringify(userNode))
.digest(`hex`);
// add it to userNode
userNode.internal.contentDigest = contentDigest;
// Create node with the gatsby createNode() API
createNode(userNode);
});
return;
}
I used axios to fetch data so you will need to install it: npm install --save axios
Explanation:
The goal is to create each node for each piece of data you want to use.
According to the createNode documentation, you have to provide an object with few required fields (id, parent, internal, children).
Once you get the results data from the randomuser API, you just need to create this node object and pass it to the createNode() function.
Here we map to the results as you wanted to get 500 random users https://randomuser.me/api/?results=500.
Create the userNode object with the required and wanted fields.
You can add more fields depending on what data you will want to use in your app.
Just create the node with the createNode() function of the Gatsby API.
2) Query your data with GraphQL
Once you did that, run gatsby develop and go to http://localhost:8000/___graphql.
You can play with GraphQL to create your perfect query. As we named the internal.type of our node object 'RandomUser', we can query allRandomUser to get our data.
{
allRandomUser {
edges {
node {
gender
name {
title
first
last
}
picture {
large
medium
thumbnail
}
}
}
}
}
3) Use this query in your Gatsby page
In your page, for instance src/pages/index.js, use the query and display your data:
import React from 'react'
import Link from 'gatsby-link'
const IndexPage = (props) => {
const users = props.data.allRandomUser.edges;
return (
<div>
{users.map((user, i) => {
const userData = user.node;
return (
<div key={i}>
<p>Name: {userData.name.first}</p>
<img src={userData.picture.medium} />
</div>
)
})}
</div>
);
};
export default IndexPage
export const query = graphql`
query RandomUserQuery {
allRandomUser {
edges {
node {
gender
name {
title
first
last
}
picture {
large
medium
thumbnail
}
}
}
}
}
`;
That is it!
Many thanks, this is working fine for me, I only change small parts of the gastbyjs-node.js because it makes an error when use sync & await, I think I need change some section of a build process to use babel to allow me to use sync or await.
Here is the code which works for me.
const axios = require('axios');
const crypto = require('crypto');
// exports.sourceNodes = async ({ boundActionCreators }) => {
exports.sourceNodes = ({boundActionCreators}) => {
const {createNode} = boundActionCreators;
return new Promise((resolve, reject) => {
// fetch raw data from the randomuser api
// const fetchRandomUser = () => axios.get(`https://randomuser.me/api/?results=500`);
// await for results
// const res = await fetchRandomUser();
axios.get(`https://randomuser.me/api/?results=500`).then(res => {
// map into these results and create nodes
res.data.results.map((user, i) => {
// Create your node object
const userNode = {
// Required fields
id: `${i}`,
parent: `__SOURCE__`,
internal: {
type: `RandomUser`, // name of the graphQL query --> allRandomUser {}
// contentDigest will be added just after
// but it is required
},
children: [],
// Other fields that you want to query with graphQl
gender: user.gender,
name: {
title: user.name.title,
first: user.name.first,
last: user.name.last
},
picture: {
large: user.picture.large,
medium: user.picture.medium,
thumbnail: user.picture.thumbnail
}
// etc...
}
// Get content digest of node. (Required field)
const contentDigest = crypto.createHash(`md5`).update(JSON.stringify(userNode)).digest(`hex`);
// add it to userNode
userNode.internal.contentDigest = contentDigest;
// Create node with the gatsby createNode() API
createNode(userNode);
});
resolve();
});
});
}
The accepted answer for this works great, just to note that there's a deprecation warning if you use boundActionCreators. This has to be renamed to actions to avoid this warning.
You can get data at the frontend from APIs using react useEffect. It works perfectly and you will no longer see any error at builtime
const [starsCount, setStarsCount] = useState(0)
useEffect(() => {
// get data from GitHub api
fetch(`https://api.github.com/repos/gatsbyjs/gatsby`)
.then(response => response.json()) // parse JSON from request
.then(resultData => {
setStarsCount(resultData.stargazers_count)
}) // set data for the number of stars
}, [])
The answers given above work, except the query in step 2 seems to only return one node for me. I can return all nodes by adding totalCount as a sibling of edges. I.e.
{
allRandomUser {
totalCount
edges {
node {
id
gender
name {
first
last
}
}
}
}
}

React Redux, how to properly handle changing object in array?

I have a React Redux app which gets data from my server and displays that data.
I am displaying the data in my parent container with something like:
render(){
var dataList = this.props.data.map( (data)=> <CustomComponent key={data.id}> data.name </CustomComponent>)
return (
<div>
{dataList}
</div>
)
}
When I interact with my app, sometimes, I need to update a specific CustomComponent.
Since each CustomComponent has an id I send that to my server with some data about what the user chose. (ie it's a form)
The server responds with the updated object for that id.
And in my redux module, I iterate through my current data state and find the object whose id's
export function receiveNewData(id){
return (dispatch, getState) => {
const currentData = getState().data
for (var i=0; i < currentData.length; i++){
if (currentData[i] === id) {
const updatedDataObject = Object.assign({},currentData[i], {newParam:"blahBlah"})
allUpdatedData = [
...currentData.slice(0,i),
updatedDataObject,
...currentData.slice(i+1)
]
dispatch(updateData(allUpdatedData))
break
}
}
}
}
const updateData = createAction("UPDATE_DATA")
createAction comes from redux-actions which basically creates an object of {type, payload}. (It standardizes action creators)
Anyways, from this example you can see that each time I have a change I constantly iterate through my entire array to identify which object is changing.
This seems inefficient to me considering I already have the id of that object.
I'm wondering if there is a better way to handle this for React / Redux? Any suggestions?
Your action creator is doing too much. It's taking on work that belongs in the reducer. All your action creator need do is announce what to change, not how to change it. e.g.
export function updateData(id, data) {
return {
type: 'UPDATE_DATA',
id: id,
data: data
};
}
Now move all that logic into the reducer. e.g.
case 'UPDATE_DATA':
const index = state.items.findIndex((item) => item.id === action.id);
return Object.assign({}, state, {
items: [
...state.items.slice(0, index),
Object.assign({}, state.items[index], action.data),
...state.items.slice(index + 1)
]
});
If you're worried about the O(n) call of Array#findIndex, then consider re-indexing your data with normalizr (or something similar). However only do this if you're experiencing performance problems; it shouldn't be necessary with small data sets.
Why not using an object indexed by id? You'll then only have to access the property of your object using it.
const data = { 1: { id: 1, name: 'one' }, 2: { id: 2, name: 'two' } }
Then your render will look like this:
render () {
return (
<div>
{Object.keys(this.props.data).forEach(key => {
const data = this.props.data[key]
return <CustomComponent key={data.id}>{data.name}</CustomComponent>
})}
</div>
)
}
And your receive data action, I updated a bit:
export function receiveNewData (id) {
return (dispatch, getState) => {
const currentData = getState().data
dispatch(updateData({
...currentData,
[id]: {
...currentData[id],
{ newParam: 'blahBlah' }
}
}))
}
}
Though I agree with David that a lot of the action logic should be moved to your reducer handler.

Resources