Ramda recursive curried function throws Maximum call stack size exceeded - javascript

I try to understand ramda a bit better and why in the following example mapTreeNode2 does not work exactly like mapTreeNode.
In a new project my state is represented in a huge tree and I want to make sure I get the fundamentals of immutable tree manipulation with ramda right.
const state = {
id: 1,
name: "first",
children: [{
id: 2,
name: "second",
children: []
}, {
id: 3,
name: "third",
children: [{
id: 4,
name: "fourth",
children: []
}]
}, {
id: 5,
name: "fifth",
children: [{
id: 6,
name: "sixth",
children: []
}]
}]
};
const hasChildren = (node) => {
var x =
(typeof node === 'object')
&& (typeof node.children !== 'undefined')
&& (node.children.length > 0);
// console.log(x, node)
return x
}
// This works
const mapTreeNode = curry( (mapFn, treeNode) => {
const newTreeNode = mapFn(treeNode);
if (!hasChildren(treeNode)) {
return newTreeNode;
}
return evolve({children: map(mapTreeNode(mapFn))}, newTreeNode)
})
// This doesn't: Maximum call stack size exceeded
const mapTreeNode2 = curry( (mapFn) => ifElse(
hasChildren,
compose(
evolve({children: map(mapTreeNode2(doStuff))}),
mapFn,
),
mapFn
))
const doStuff = ifElse(propEq('id', 6), evolve({name: concat("x")}), identity)
// This works
mapTreeNode(doStuff)(state)
// This doesn't: Maximum call stack size exceeded
mapTreeNode2(doStuff)(state)

You don't need any currying here since the function has only a single parameter anyway. But that's not the issue, it doesn't work without curry either. To see why, let's refactor the function a bit:
const mapTreeNode2 = (mapFn) => {
const mapper = mapTreeNode2(mapFn);
const evolver = evolve({children: map(mapper)});
const then = compose(evolver, mapFn);
return ifElse(hasChildren, then, mapFn);
};
Right there in the first line we have an unbounded recursion… The problem is that JS is using strict evaluation, so the code of the then branch still gets evaluated with ifElse, unlike a proper if/else statement or conditional operator expression.
To avoid that, you can try
const mapTreeNode2 = (mapFn) => {
const evolver = evolve({children: map(mapper)});
const then = compose(evolver, mapFn);
const mapper = ifElse(hasChildren, then, mapFn);
return mapper;
};
but again that won't work without lazy evaluation, accessing mapper in the temporal dead zone. The only way to resolve this circular reference between variables in JS is to use a function and utilise hoisting, e.g.
const mapTreeNode2 = (mapFn) => {
const mapper = (treeNode) => {
const evolver = evolve({children: map(mapper)});
// ^^^^^^ now the reference works
const then = (node) => compose(evolver, mapFn);
return ifElse(hasChildren, then, mapFn)(treeNode);
};
return mapper;
};

Related

How I can operate on the current state right after updating it in the same function?

Essentially I have this state object and this method:
const [groupData, setGroupData] = useState({});
// groupData state
groupData = {
group1: [
{ id: 1, name: Mike, age: 24 },
{ id: 2, name: Bob, age: 31 }
],
group2: [
{ id: 3, name: Christin, age: 21 },
{ id: 4, name: Michelle, age: 33 }
],
}
const stateRef = useRef();
stateRef.current = groupData;
const handleRemovePerson = (personToRemoveById: string) => {
const filteredGroupData = Object.fromEntries(
Object.entries(groupData).map(([key, value]) => {
return [key, value.filter((person) => person.id !== personToRemoveById)];
}),
);
setMainContentData(filteredGroupData);
// Now check if group array does not have anymore persons, if empty then delete
group array
console.log('stateRef', stateRef);
// outputs the correct current data
const filteredStateRef = Object.keys(stateRef).map((key) => stateRef[key]);
console.log('filteredStateRef', filteredStateRef);
// outputs previous data ??
};
I tried useRef and once I loop through it, it gives me back the previous data. How can I get the most current data after setting state and then operating on that new data right away? Thank you!
First of all, you can't access the state after using setState because it's an asynchronous operation and if you want to check something after using setState you need use useEffect hook to listen for state change and decide what to do with it, or in recent versions of react and react-dom you could use a not so suggested function called flushSync which will would update the state synchronously.
so the prefered way is to use useEffect like this:
const handleRemovePerson = (personToRemoveById: string) => {
const filteredGroupData = Object.fromEntries(
Object.entries(groupData).map(([key, value]) => {
return [key, value.filter((person) => person.id !== personToRemoveById)];
}),
);
setMainContentData(filteredGroupData);
};
useEffect(() => {
if(true/**some conditions to prevents extra stuff */){
// do your things, for example:
console.log('stateRef', stateRef);
// outputs the correct current data
const filteredStateRef = Object.keys(stateRef).map((key) => stateRef[key]);
}
}, [mainContentData, /**or other dependencies based on your needs */])

React setState of for deeply nested value

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

RxJs: Paginate through API recursively and find value from list

I am using rxjs v6.4.0. I am trying to paginate through an API searching for a very specific channel where name equals "development". I am using expand to recursively call the API and get new pages. The end result gives me a concatenated list of channels. Then I filter out all channels where name not equal to "development". However I am getting an error: TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.
const Rx = require('rxjs')
const Rx2 = require('rxjs/operators')
const getChannel = (cursor) => {
return this.service.getData(`${url}?cursor=${cursor || ''}`)
.pipe(Rx2.map(resp => JSON.parse(resp.body)))
.pipe(Rx2.expand(body => { // recurse the api until no more cursors
return body.response_metadata &&
body.response_metadata.next_cursor ?
getChannel(body.response_metadata.next_cursor) : Rx.EMPTY
}))
.pipe(Rx2.pluck('channels'))
.pipe(Rx2.mergeAll()) // flattens array
.pipe(Rx2.filter(c => {
console.log('finding', c.name === 'development')
return c.name === 'development'
}))
}
The find callback should return a boolean, not an Observable. E.g.
find(c => c.name === 'development')
UPDATE
Heres a modified example of yours. I've removed generators as they are more complicated then our case needs.
const { of, EMPTY, throwError } = rxjs;
const { filter, tap, expand, pluck, mergeAll } = rxjs.operators;
const DATA =
[ {channels: [{id: 123, name: 'test'}, {id:4, name: 'hello'}], cursor: 1}
, {channels:[{id: 1, name: 'world'}, {id: 2, name: 'knows'}], cursor: 2}
, {channels:[{id: 3, name: 'react'}, {id: 5, name: 'devcap'}], cursor: false}
];
function getChannel(){
return getBlock()
.pipe(
expand(x => x.cursor ? getBlock(x.cursor) : EMPTY),
pluck('channels'),
mergeAll(),
filter(c => c.name === 'devcap')
)
}
getChannel().subscribe({
next: console.log,
error: console.error
});
function getBlock(index = 0) {
if (index >= DATA.length){
throwError('Out of bounds');
}
return of(DATA[index]);
}
UPDATE 2
Your solution didn't work due to recursion being done through solely getChannel(). When you do the expand -- you run another cycle through getChannel(). Meaning that you run pluck-mergeAll-filter chain twice on each recursively fetched value! Plucking and flattering it twice gives you undefined -- therefore the error.
In your playground -- try separating out this code
let getValue = ()=>{
const next = gen.next();
if (!next || !next.value) { return EMPTY; }
return next.value;
}
and use it in the expand, like this:
let getChannel = () => {
return getValue()
.pipe(
expand(body => {
return body.cursor ? getValue() : EMPTY
}),
pluck('channels'),
mergeAll(),
filter(c => c.name === 'devcap'),
)
}
Let me know if this is the functionality you are looking for https://codepen.io/jeremytru91/pen/wOQxbZ?editors=1111
const {
of,
EMPTY
} = rxjs;
const {
filter,
tap,
expand,
take,
pluck,
concatAll,
flatMap,
first
} = rxjs.operators;
function* apiResponses() {
yield of({channels: [{id: 123, name: 'test'}, {id:4, name: 'hello'}], cursor: 1});
yield of({channels:[{id: 3, name: 'react'}, {id: 5, name: 'devcap'}], cursor:3});
yield of({channels:[{id: 1, name: 'world'}, {id: 2, name: 'knows'}], cursor: 2});
yield of({channels:[{id: 4, name: 'react'}, {id: 6, name: 'devcap'}], cursor:4});
}
let gen = apiResponses();
function getChannel() {
return gen.next().value // simulate api call
.pipe(
expand(body => {
return body.cursor ? gen.next().value : EMPTY
}),
pluck('channels'),
flatMap(channels => {
const filtered = channels.filter(channel => channel.name === 'devcap')
if(filtered.length) {
return filtered;
}
return EMPTY;
}),
first()
);
}
getChannel().subscribe(data => {
console.log('DATA!! ', data)
}, e => {
console.log('ERROR', e)
throw e
})

React destructuring applied in a native JS project

I'm going through a React + Redux tutorial where there is a code snippet such as this:
class ExampleComponent extends Component {
constructor() {
super();
this.state = {
articles: [
{ title: "React Redux Tutorial for Beginners", id: 1 },
{ title: "Redux e React: cos'è Redux e come usarlo con React", id: 2 }
]
};
}
render() {
const { articles } = this.state;
return <ul>{articles.map(el => <li key={el.id}>{el.title}</li>)}</ul>;
}
}
Then, out of curiosity I did a simillar thing but in a non-react environment (eg. in console log in the browser). First I initialized a constant like this:
const articles = [
{ title: "React Redux Tutorial for Beginners", id: 1 },
{ title: "Redux e React: cos'è Redux e come usarlo con React", id: 2 }
]
But what confuses me is the destructuring part after which I get undefined. Like this:
const { someObj } = articles;
undefined
someObj
undefined
{ someObj }
{someObj: undefined}
someObj.title
VM205:1 Uncaught TypeError: Cannot read property 'title' of undefined
at <anonymous>:1:9
My question is, why const { articles } = this.state; works fine, but const { someObj } = articles; returns undefined?
You are trying in the wrong way. You have an object here and it has an array:
const obj = { arr: [ 1, 2, 3 ] };
Instead of using like obj.arr you are destructuring the arr like that:
const { arr } = obj;
And use that variable directly with its name as arr.
If you want to destructure any item from an array you can use array destructring:
const [ number1, number2, number3 ] = arr;
If you want to combine those steps:
const { arr: [ number1, number2, number3 ] } = obj;
So right side is the destructured variable and the left side is the variables that you are destructring from the original one.
Also see:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
Because this.state is an Object, and your articles variable is an Array.
Object destructing syntax:
const { var1, var2 } = obj;
is equal to
const var1 = obj.var1;
const var2 = obj.var2;
Array destructing syntax:
const [arr1, arr2, ...arr3] = arr;
is equal to
const arr1 = arr[0];
const arr2 = arr[1];
const arr3 = arr.slice(2);
You can see more here:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

Checking if Ticker is in another list, then adding a new key [RamdaJS]

I'm trying to use Ramda to compare 2 lists, find which tickers in the tickersList are also in the portfolioTickers. If one is in the portfolio list as well then add a key portfolio:true.
Example data:
tickers = [
{ ticker: aa },
{ ticker: bb },
{ ticker: cc }
]
portfolio = [
{ ticker: aa }
]
Expected result:
tickers = [
{ ticker: aa, portfolio: true },
{ ticker: bb },
{ ticker: cc }
]
Sounds simple, but stuck here so far:
const checkTicker = _.curry((ticker, portTicker) => {
if (ticker.ticker === portTicker.ticker) {
ticker.portfolio = true;
}
return ticker;
});
const matched = R.anyPass([checkTicker]);
const setPortfolioFlag = _.curry((portfolioTickers, ticker) => {
// const matched = R.filter(checkTicker(ticker), portfolioTickers);
// console.log('matched', matched)
const each = matched(R.forEach(checkTicker(ticker), portfolioTickers));
console.log('each', each)
return each;
});
const inPortfolioCheck = (tickersList) => {
const portfolioTickers = R.filter(TickersFactory.isTickerInPortfolio, tickersList);
const tickers = R.map(setPortfolioFlag(portfolioTickers), tickersList);
console.log('tickers', tickers)
};
const displayTickers = (tickers) => {
this.tickersList = tickers;
const checkedTickers = inPortfolioCheck(this.tickersList);
console.log('checkedTickers', checkedTickers)
};
Right now each is always true, and the list that all this logic returns is just a list the same length as my tickerList, but just true'.
A problem I keep running into is I feel that I need to run another 2nd R.map to do the check, however the result of that returned map is the Portfolio tickers, and not the original list.
Working code, but with an ugly for loop:
This obviously works because I'm using a for loop, but I'm trying to remove all object oriented code and replace with functional code.
const setPortfolioFlag = _.curry((portfolioTickers, ticker) => {
for (var i=0; i<portfolioTickers.length; i++) {
if (portfolioTickers[i].ticker === ticker.ticker) {
ticker.portfolio = true;
}
}
return ticker;
});
const inPortfolioCheck = (tickersList) => {
const portfolioTickers = R.filter(TickersFactory.isTickerInPortfolio, tickersList);
return R.map(setPortfolioFlag(portfolioTickers), tickersList);
};
const displayTickers = (tickers) => {
this.tickersList = inPortfolioCheck(tickers);
console.log('this.tickersList', this.tickersList)
};
LoDash version of the forLoop:
_.each(portfolio, (port) => {
if (port.ticker === ticker.ticker) {
ticker.portfolio = true;
}
});
So the first thing you might notice is that the expected resulting tickers array is the same "shape" as the input tickers array, suggesting that we should be able to make use of R.map for the expression. Knowing that, we can then focus on what just has to happen to the individual elements of the array.
So for each ticker object, when found in the portfolio array, we would like to attach the portfolio: true property.
const updateTicker =
R.when(R.flip(R.contains)(portfolio), R.assoc('portfolio', true))
updateTicker({ ticker: 'aa' })
//=> {"portfolio": true, "ticker": "aa"}
updateTicker({ ticker: 'bb' })
//=> {"ticker": "bb"}
Then we can just map over the tickers list to update each ticker.
R.map(updateTicker, tickers)
//=> [{"portfolio": true, "ticker": "aa"}, {"ticker": "bb"}, {"ticker": "cc"}]
The answer from #scott-christopher is, as always, excellent. But subsequent comments make me think that it doesn't quite cover your use case. If the data is more complex, and it's possible that items in the portfolio and the tickers might have distinct properties, then contains won't be strong enough. So if your data looks more like this:
const tickers = [
{ ticker: 'aa', size: 1 },
{ ticker: 'bb', size: 2 },
{ ticker: 'cc', size: 3 },
{ ticker: 'dd', size: 4 }
]
const portfolio = [
{ ticker: 'aa', id: 'abc' },
{ ticker: 'dd', id: 'xyz' }
]
Then this version might work better:
const updateTicker =
when(pipe(eqProps('ticker'), any(__, portfolio)), assoc('portfolio', true))
This nearly points-free version might be a little obscure. It's equivalent to this one:
const updateTicker =
when(
(ticker) => any(eqProps('ticker', ticker), portfolio),
assoc('portfolio', true)
)
Also, if the any(__, portfolio) notation is unclear, it's equivalent to flip(any)(portfolio).
You can see this on the Ramda REPL.

Categories

Resources