I've got state with a nested array that looks like the following:
{
list: [
{
id: '3546f44b-457e-4f87-95f6-c6717830294b',
title: 'First Nest',
key: '0',
children: [
{
id: '71f034ea-478b-4f33-9dad-3685dab09171',
title: 'Second Nest',
key: '0-0
children: [
{
id: '11d338c6-f222-4701-98d0-3e3572009d8f',
title: 'Q. Third Nest',
key: '0-0-0',
}
],
}
],
],
selectedItemKey: '0'
}
Where the goal of the nested array is to mimic a tree and the selectedItemKey/key is how to access the tree node quickly.
I wrote code to update the title of a nested item with the following logic:
let list = [...state.list];
let keyArr = state.selectedItemKey.split('-');
let idx = keyArr.shift();
let currItemArr = list;
while (keyArr.length > 0) {
currItemArr = currItemArr[idx].children;
idx = keyArr.shift();
}
currItemArr[idx] = {
...currItemArr[idx],
title: action.payload
};
return {
...state,
list
};
Things work properly for the first nested item, but for the second and third level nesting, I get the following Immer console errors
An immer producer returned a new value *and* modified its draft.
Either return a new value *or* modify the draft.
I feel like I'm messing up something pretty big here in regards to my nested array access/update logic, or in the way I'm trying to make a new copy of the state.list and modifying that. Please note the nested level is dynamic, and I do not know the depth of it prior to modifying it.
Thanks again in advance!
Immer allows you to modify the existing draft state OR return a new state, but not both at once.
It looks like you are trying to return a new state, which is ok so long as there is no mutation. However you make a modification when you assign currItemArr[idx] = . This is a mutation because the elements of list and currItemArr are the same elements as in state.list. It is a "shallow copy".
But you don't need to worry about shallow copies and mutations because the easier approach is to just modify the draft state and not return anything.
You just need to find the correct object and set its title property. I came up with a shorter way to do that using array.reduce().
const keyArr = state.selectedItemKey.split("-");
const target = keyArr.reduce(
(accumulator, idx) => accumulator.children[idx],
{ children: state.list }
);
target.title = action.payload;
Related
I have a functional component, that is passed instructions on what to pull from the redux store.
Using mapStateToProps=(state, ownProps), I can happily pull the required items from state (store) - but, at a cost of any changes in the entire state tree triggering rerunning mapStateToProps and a gazillion rerenders.
Let me unpack.
Here's a snapshot of part of the store:
{
settings: {...stuff...},
projects: [...stuff...],
definitions: [...stuff...],
themes: [...stuff...],
surfaces: {
'6': { <--- VARIABLE PASSED TO COMPONENT
surface: {
STRIP: [..stuff..],
GLOBAL: { <--- CATEGORY PASSED TO COMPONENT
DISPLAY: {...stuff...},
ASSIGNMENT: { <--- LIST OF REQUIRED OBJECTS HAS
A_TRACK: { SUBCATEGORY AND TARGET (A_TRACK etc...)
value: 0,
type: 'switch',
label: 'TRACK'
},
A_SEND: { <--- ANOTHER OBJECT I NEED TO GET
value: 0,
type: 'switch',
label: 'SEND'
},
A_PAN: {
value: 0,
type: 'switch',
label: 'PAN'
},
},
FADER_BANKS: {...stuff...},
STATUS: {...stuff...},
LOTS_MORE_STUFF
My parent component passes the required instructions to the child.
<RefMixerGroup
portId = {this.props.portId}
items={[
{parent: 'GLOBAL', group: "ASSIGNMENT", target: "A_TRACK"},
{parent: 'GLOBAL', group: "ASSIGNMENT", target: "A_SEND"},
]
}
/>
mapStateToProps is pretty simple:
const mapStateToPropy = (state, ownProps) => {
return {
groupItems: getItemsFromState(state.surfaces[ownProps.portId].surface, ownProps.items)
}
}
and the work is done in a simple function:
const getItemsFromState = (subState, items)=>{
let groupItems=[]
for (let i = 0; i < items.length; i++) {
const item = items[i];
const base = subState[item.parent];
let groupItem = base[item.group][item.target]
groupItems.push({...groupItem, target: item.target})
}
return groupItems
}
But because I am creating this array of matches, I think redux thinks I should be subscribing to every item in the tree...when I only want changes on the found elements, in this case:
surfaces[6].surface[GLOBAL][ASSIGNMENT][A_TRACK]
surfaces[6].surface[GLOBAL][ASSIGNMENT][A_SEND]
I tried using reselect and the rereselect instead of my getItemsFromState function above,
but all with the same result. Any change in that tree, starting with surfaces[6] triggers mapsStateToProps and a rerender.
There must be way around this, but I can't figure it out. I tried using areStatesEqual but it only provides nextState and prevState, and I need ownProps to compute equality. I possibly could use areStatePropsEqual, but that only works AFTER recomputing mapStateToProps unnecessarily.
There must be a way!
getItemsFromState is creating a new groupItems array reference every time it runs. It will be called after every dispatched action. Since connect re-renders any time any of the fields returned by mapState have changed to a new reference, your code is forcing React-Redux to re-render every time.
This is specifically why you should use memoized selectors to only return new derived data references if the input references have changed, typically with Reselect's createSelector. If your use of Reselect isn't helping here, it's likely that your selectors aren't being set up correctly, but I'd need to see specific examples to give advice there.
It's also why components should subscribe to the smallest amount of data that they actually need.
If you are using a function component, I'd suggest using useSelector instead of connect as well.
I'm using metascraper in a project I'm working on. I'm passing in custom rules into the contructor. It's actually scraping actual content from the page its scraping. The problem is, is that it appears to be finding every tag that matches the CSS selector, and combining all of the text() content from every tag on the page. I checked metascraper website and github and couldn't find any information about an option that changes this kind of mode/behavior. I made sure that each scrape request creates a new instance of metascraper in case it was just using the same member variables across multiple uses of the object, but that didn't seem to do anything. Any thoughts?
Edit: Also, ideally, metascraper would return an array of arrays of sets of selectors it finds. I have 4 selectors in a group that appear in groups throughout a page. I need it to iterate over the selectors in order, until it cannot find any more instances of the 1st selector (aka the groups have stopped appearing on the page).
type4: async (page: Page): Promise<Extract[]> => {
const html = await page.content()
const url = await page.url()
const type4MetascraperInstance = createType4MetaScraperInstance()
const metadata = await type4MetascraperInstance({ html: html, url: url })
console.log('metadata: ', metadata)
const extract: Extract[] = [{
fingerprint: 'type4',
author: metadata.author,
body: metadata.description,
images: null,
logo: null,
product: null,
rating: null,
title: metadata.title,
videos: null
}]
return extract
}
The function for creating the Type4 metascraper instance is:
function createType4MetaScraperInstance() {
const toDescription = toRule(description)
const toAuthor = toRule(author)
const toTitle = toRule(title, { removeSeparator: false })
const type4MetaScraperInstance = metaScraper([ {
author: [
toAuthor($ => $('.a-profile-name').text()),
],
title: [
toTitle($ => $('a[data-hook="review-title"] > span').text()),
],
description: [
toDescription($ => $('.review-text-content').text()),
]
} ])
return type4MetaScraperInstance
}
I decided to architect a different solution here that uses a python script to properly parse reviews, and it will need to read/write to google cloud datastore. Some suggestions people provided were to write my own calls to cheeriojs (https://cheerio.js.org/), instead of using metascraper at all.
I'm using the Nodejs library for talking to Jira called jira-connector. I can get all of the boards on my jira instance by calling
jira.board.getAllBoards({ type: "scrum"})
.then(boards => { ...not important stuff... }
the return set looks something like the following:
{
maxResults: 50,
startAt: 0,
isLast: false,
values:
[ { id: ... } ]
}
then while isLast === false I keep calling like so:
jira.board.getAllBoards({ type: "scrum", startAt: XXX })
until isLast is true. then I can organize all of my returns from promises and be done with it.
I'm trying to reason out how I can get all of the data on pages with Ramda, I have a feeling it's possible I just can't seem to sort out the how of it.
Any help? Is this possible using Ramda?
Here's my Rx attempt to make this better:
const pagedCalls = new Subject();
pagedCalls.subscribe(value => {
jira.board.getAllBoards({ type:"scrum", startAt: value })
.then(boards => {
console.log('calling: ' + value);
allBoards.push(boards.values);
if (boards.isLast) {
pagedCalls.complete()
} else {
pagedCalls.next(boards.startAt + 50);
}
});
})
pagedCalls.next(0);
Seems pretty terrible. Here's the simplest solution I have so far with a do/while loop:
let returnResult = [];
let result;
let startAt = -50;
do {
result = await jira.board.getAllBoards( { type: "scrum", startAt: startAt += 50 })
returnResult.push(result.values); // there's an array of results under the values prop.
} while (!result.isLast)
Many of the interactions with Jira use this model and I am trying to avoid writing this kind of loop every time I make a call.
I had to do something similar today, calling the Gitlab API repeatedly until I had retrieved the entire folder/file structure of the project. I did it with a recursive call inside a .then, and it seems to work all right. I have not tried to convert the code to handle your case.
Here's what I wrote, if it will help:
const getAll = (project, perPage = 10, page = 1, res = []) =>
fetch(`https://gitlab.com/api/v4/projects/${encodeURIComponent(project)}/repository/tree?recursive=true&per_page=${perPage}&page=${page}`)
.then(resp => resp.json())
.then(xs => xs.length < perPage
? res.concat(xs)
: getAll(project, perPage, page + 1, res.concat(xs))
)
getAll('gitlab-examples/nodejs')
.then(console.log)
.catch(console.warn)
The technique is pretty simple: Our function accepts whatever parameters are necessary to be able to fetch a particular page and an additional one to hold the results, defaulting it to an empty array. We make the asynchronous call to fetch the page, and in the then, we use the result to see if we need to make another call. If we do, we call the function again, passing in the other parameters needed, the incremented page number, and the merge of the current results and the ones just received. If we don't need to make another call, then we just return that merged list.
Here, the repository contains 21 files and folders. Calling for ten at a time, we make three fetches and when the third one is complete, we resolve our returned Promise with that list of 21 items.
This recursive method definitely feels more functional than your versions above. There is no assignment except for the parameter defaulting, and nothing is mutated along the way.
I think it should be relatively easy to adapt this to your needs.
Here is a way to get all the boards using rubico:
import { pipe, fork, switchCase, get } from 'rubico'
const getAllBoards = boards => pipe([
fork({
type: () => 'scrum',
startAt: get('startAt'),
}),
jira.board.getAllBoards,
switchCase([
get('isLast'),
response => boards.concat(response.values),
response => getAllBoards(boards.concat(response.values))({
startAt: response.startAt + response.values.length,
})
]),
])
getAllBoards([])({ startAt: 0 }) // => [...boards]
getAllBoards will recursively get more boards and append to boards until isLast is true, then it will return the aggregated boards.
In the example's src(UserPage.js):
const mapStateToProps = (state, ownProps) => {
// We need to lower case the login due to the way GitHub's API behaves.
// Have a look at ../middleware/api.js for more details.
const login = ownProps.params.login.toLowerCase()
const {
pagination: { starredByUser },
entities: { users, repos }
} = state
const starredPagination = starredByUser[login] || { ids: [] }
const starredRepos = starredPagination.ids.map(id => repos[id])
const starredRepoOwners = starredRepos.map(repo => users[repo.owner])
return {
login,
starredRepos,
starredRepoOwners,
starredPagination,
user: users[login]
}
}
I notice that there is many templates like xxx.ids.map(id => someEntities[id]),I am not sure why use this pattern to work.IMO,I would use something like import { map } from 'lodash'; someList && map(someList, item => {...}) in the container component and just pass the entities in the mapStateToProps.
So,could someone explains it's purpose?Thanks.
The standard suggestion for normalizing data in Redux is to store data items in an object, with IDs as the keys and the items as the values. However, an object doesn't have an inherent order to it. (Technically, the order of iteration for object keys should be consistent, but it's a bad practice to rely on that as the sole means of ordering.)
Because of that, it's also standard to store an array of just the IDs as well. A typical example might look like:
{
byId : {
qwerty : { },
abcd : { },
aj42913 : { }
},
items : ["qwerty", "aj42913", "abcd"],
sorted : ["abcd", "aj42913", "qwerty"],
selected : ["qwerty", "abcd"]
}
In this example, items contains all item IDs, probably in the order they were insert. sorted contains the IDs in some sort of sorted order, while selected contains a subset of the IDs.
This allows the items themselves to only be stored once, while multiple representations of those items can be saved using various arrays of IDs.
From there, you can pull together a list of the actual items by mapping over whatever array of IDs you care about, and retrieving the items by their IDs.
So, ultimately the answer is that relying just on the keys of the byId object doesn't give you any kind of ordering, and doesn't allow defining subsets of the data.
I have an immutable.js map. For example:
// default, when user first gets on page
var myObject = Immutable.Map({
productID: '',
colors: ['brown', 'red'],
sku: ''
sizes: [10]
})
Now, depending on how they get to my app - I populate that above "myObject" with different data.
so, for example: lets say they come from pathA
// pathA passes in some data... hydrate the myObject
var myObject = Immutable.Map({
productID: '090ABl',
colors: ['brown', 'red', 'yellow'],
sku: '__whatever'
sizes: [10, 18, 9]
})
so, for example: lets say they come from pathB
** this is where the issue comes from. I have that previous "state" of myObject hanging around. I need to "clear and go back to the initial state". I am using redux.
// pathB passes in some data... hydrate the myObject
var myObject = Immutable.Map({
productID: '090XZLG',
colors: ['red', 'yellow'],
sku: '__food'
sizes: [9]
})
The data is combing etc.. I need it to "clear out.". Curious if there is an Immutable.js method that enables to refresh the myObject with a new one, that is the same as the initial state. I am new to immutable.js so I am a bit curious about why its so hard to do simple things :-)
When using immutable for the state object in redux, in many of my reducers I set a certain key to what I get from the initial state.
i.e.:
function reeducer(state = initialState, action) {
switch (action.type) {
...
case CLEAR_SELECTION:
return state.set('selected', initialState.get('selected'));
...
}
}
And of course to make it a bit cleaner (specially to avoid repeating a string) one could create a function for this:
function resetKey(state, initialState, key) {
return state.set(key, initialState.get(key));
}
(Alternatively it could be called copyKey or copySection)
Or it could be a resetPath/copyPath
function resetPath(state, initialState, path) {
return state.setIn(key, initialState.getIn(path));
}
The immutable.js object does not 'know' what its 'defaults' are, so there is no way of resetting it. If you have a default object in your application just keep a reference to that in a variable, like you would if it were a plain JavaScript object:
var defaultObject = Immutable.Map({
productID: '',
colors: ['brown', 'red'],
sku: ''
sizes: [10]
})
Then when the user arrives at a certain path, you simply make your modifications and keep them in a different variable:
var myObject = defaultObject.set(...);