JS spread operator workflow on React - javascript

React suggests not to mutate state. I have an array of objects which I am manipulating based on some events. My question is, is it okay to write it like this:
const makeCopy = (arr) => arr.map((item) => ({ ...item }));
function SomeComponenet() {
const [filters, setFilters] = useState(aemFilterData);
const handleFilterClick = (filter, c) => {
let copiedFilters = makeCopy(filters);
/**
* Apply toggle on the parent as well
*/
if (!("parentId" in filter)) {
copiedFilters[filter.id].open = !copiedFilters[filter.id].open;
}
setFilters(copiedFilters);
}
}
Am I mutating the original object by doing like above? Or does it make a difference if written like this:
const makeCopy = (arr) => arr.map((item) => ({ ...item }));
function SomeComponent() {
const [filters, setFilters] = useState(aemFilterData);
const handleFilterClick = (filter, c) => {
let copiedFilters = makeCopy(filters);
/**
* Apply toggle on the parent as well
*/
if (!("parentId" in filter)) {
copiedFilters = copiedFilters.map((f) => {
if (filter.id === f.id) {
return {
...f,
open: !f.open,
};
} else {
return { ...f };
}
});
}
setFilters(copiedFilters);
}
}
What's the preferred way to do this? Spread operators are getting a lot verbose and I am not liking it, but I prefer it if that's how I need to do it here. immutable.js and immer or not an option right now.

const makeCopy = (arr) => arr.map((item) => item );
With above code, it's mutating on the original object reference because we're not creating a deep clone.
copiedFilters[filter.id].open = !copiedFilters[filter.id].open;
Here reference of copiedFilters[filter.id] and filters[filter.id] is same.
With spread operator
const makeCopy = (arr) => arr.map((item) => ({ ...item }));
Here we create a new copy of the inner object too. So copiedFilters[filter.id] and filters[filter.id] will have different reference.
This is same as your second approach.
So either you use spread operator while making a copy or you can skip making a copy in the second approach and directly map on filters since you're using spread operator there. This looks better because, why run loop twice - first to create copy and then to update open.
// let copiedFilters = makeCopy(filters); Not needed in second approach
copiedFilters = copiedFilters.map((f) => {
if (filter.id === f.id) {
return {
...f,
open: !f.open,
};
} else {
return { ...f };
}
});
You can create a deep clone when you copy but that would be waste of computation and memory, I don't think it's needed here.
Deep clone is helpful when you have further nesting in the object.

Related

no-unused-expressions on ternary

Here I have an error that says:
Expected an assignment or function call and instead saw an expression
no-unused-expressions
I want onClick put my const handleCompleted with ternary. I think it's just a problem with {} or (), I did lots of things but I always have this error message.
const Filters = ({ listTask, setListTask, filters, setFilters }) => {
const [completed, setCompleted] = useState(false);
const handleCompleted = () => {
const newFilters = { ...filters };
{
completed
? (setCompleted(false),
(newFilters.status.completed = ""),
setFilters(newFilters))
: (setCompleted(true),
(newFilters.status.completed = "Completed"),
setFilters(newFilters));
}
};
return(...);
};
There is some redundant logic (setFilters(newFilters) twice), in addition, you only made a shallow copy of filters ({...filters}), therefore you mutate state (newFilters.status.completed = ...).
I would rewrite it to something like:
const Filters = ({ listTask, setListTask, filters, setFilters }) => {
const [completed, setCompleted] = React.useState(false);
const handleCompleted = () => {
setFilters((filters) => ({
...filters,
status: { ...filters.status, completed: completed ? "" : "Completed" }
}
));
setCompleted(prev => !prev);
};
return <>...</>;
};
I'm pretty sure, it has to do with the weird ternary construction. It's probably just an error in ESLint.
BUT: A ternary operator should be used for conditionally assigning values to variables.
Although it CAN be used as a replacement for if (), it SHOULD NOT be used that way.
The reason is simple: It's unreadable and is prone to unwanted side effects that are hard to debug. Just don't ever use ternaries as a replacement for if ().
Try this instead. I think you find it even more readable.
const Filters = ({ listTask, setListTask, filters, setFilters }) => {
const [completed, setCompleted] = useState(false);
const handleCompleted = () => {
const newFilters = { ...filters };
// this is how you should use ternaries:
newFilters.status.completed = completed ? '' : 'Completed'
setCompleted(!completed)
setFilters(newFilters)
};
return(...);
};

Is there a javascript universal chaining method

WARNING: Axios is broken if you use this, but fetch is not.
For short I'm asking if there is a standard way to chain functions independently of type, with a similar implementation which doesn't polute Native Prototypes:
Object.prototype.pipe = function (func) {
return func(this)
}
Edit: Use the following if you want to avoid enumerable bugs where the method would appear when enumerating keys of any object, also it's a loose defined method.
Object.defineProperty(Object.prototype, 'pipe', {
value(func) {
return func(this)
}
})
This would permit to do thing like :
const preloadContext = require.context('#/', true, /\.preload\.vue$/)
const preloadComponents = preloadContext
.keys()
.map(fileName => {
const name = fileName
.split('/')
.pop()
.replace(/\.\w+.\w+$/, '')
.pipe(camelCase)
.pipe(upperFirst)
const component = filename
.pipe(preloadContext)
.pipe(config => config.default || config)
Vue.component(name, component)
return [name, component]
})
.pipe(Object.fromEntries)
instead of
const preloadContext = require.context('#/', true, /\.preload\.vue$/)
const preloadComponents = Object.fromEntries(preloadContext
.keys()
.map(fileName => {
const name = upperFirst(camelCase(fileName
.split('/')
.pop()
.replace(/\.\w+.\w+$/, '')
))
const config = preloadContext(fileName)
const component = config.default || config
Vue.component(name, component)
return [name, component]
})
)
No, there’s no real standard way. There’s a stage 1 proposal for an operator that would do it, though:
const name = fileName
.split('/')
.pop()
.replace(/\.\w+.\w+$/, '')
|> camelCase
|> upperFirst
which you can use today with rewriting tools like Babel.
If you don’t want to use a rewriting tool, though, I definitely wouldn’t create Object.prototype.pipe. There are still cleaner non-ideal options:
const pipe = (value, fns) =>
fns.reduce((acc, fn) => fn(acc), value);
const pipe = (value, fns) =>
fns.reduce((acc, fn) => fn(acc), value);
const foo = pipe('Mzg0MA==', [
atob,
Number,
x => x.toString(16),
]);
console.log(foo);
(Lots of helpful function collections and individual packages implement this.)

Svelte derived stores and array sort

I set up a store containing a list of rides loaded from my API:
const loadRides = () => client.service('rides').find({
query: {
$sort: {
date: -1,
}
}
});
const createRides = () => {
const { subscribe, update } = writable([], async (set) => {
try {
const rides = await loadRides().then((result) => result.data);
set(rides);
} catch (e) {
console.error(e);
}
// Socket update binding?
});
subscribe((rides) => console.debug('rides', rides));
return {
subscribe,
refresh: () => loadRides().then((result) => update(() => result.data)),
};
};
export const rides = createRides();
Then I set a two derived stores for past and future rides:
export const pastRides = derived(
rides,
($rides) => $rides
.filter((ride) => ride.steps.every((step) => step.doneAt))
,
);
export const comingRides = derived(
rides,
($rides) => $rides
.filter((ride) => ride.steps.some((step) => !step.doneAt))
.sort((rideA, rideB) => {
const compare = new Date(rideA.date) - new Date(rideB.date);
console.log(rideA.date, rideB.date, compare);
return compare;
})
,
);
The sort method on the second one does not have any effect.
So I tried to put this method before the filter one. It works, but it also sort $pastRides. In fact, it is sorting the full $rides array and it make sens.
But I does not understand why the sort after filter does not work.
What did I miss?
Array.sort is mutable. Meaning, when you call rides.sort, it will sort and modify rides and return the sorted rides.
When you use derived(rides, ($rides) => ... ), the $rides you received is the original rides array that you call set(rides). So you can imagine that both the pastRides and comingRides received the same $rides array.
you can observe this behavior in this repl
To not having both derived interfere with each other, you can create a new array and sort the new array:
const sorted_1 = derived(array, $a => [...$a].sort());
you can try this out in this repl.

How to write jest test script to check the value of constants

I have a list of constants defined like this
const actions = {}
// Home
actions.HOME = {}
actions.HOME.SET_PROFILE_ID = 'SET_PROFILE_ID'
actions.HOME.LOAD_PROFILE = 'HOME_LOAD_PROFILE'
actions.HOME.SET_PROFILE = 'HOME_SET_PROFILE'
actions.OUTSIDE = {}
actions.OUTSIDE.UPDATE_PROFILE_ID = 'SET_PROFILE_ID' // this should error
module.exports = actions
The objects with in objects is to help intelisense so devs can narrow down as they go.
I want to use jest to write a test that will check to make sure no 2 constants have the same value, no matter the depth, otherwise it can create very odd errors that are hard to debug at run time. I don't really understand the documentation and how I can do this. https://jestjs.io/docs/en/using-matchers But this is my first time making any unit tests.
Thank you
-Edit
This is what I have so far. Based on Jared Smiths comments, I am no where close to the right answer as this is too simple. It only finds the first mistake, not all of them.
describe('Actions.js', () => {
it('verify no duplicate action values', () => {
const flattenActions = []
_.forEach(actions, store => {
_.forEach(store, action => {
flattenActions.push(action)
})
})
const testedActions = []
_.forEach(flattenActions, action => {
expect(testedActions).not.toContain(action)
testedActions.push(action)
})
})
})
First of all you can get all the values of your actions
function getDeepValues(obj) {
let values = [];
for (const key in obj) {
if (typeof obj[key] === 'object') {
const subVals = getDeepValues(obj[key]);
values = [...values, ...subVals];
} else {
values.push(obj[key]);
}
}
return values;
}
Will output something like this:
[ 'SET_PROFILE_ID', 
'HOME_LOAD_PROFILE', 
'HOME_SET_PROFILE', 
'SET_PROFILE_ID' ]
And then you test if the array doesn't contain any duplicates:
function arrayHasNoDuplicate(arr) {
return arr.every(num => arr.indexOf(num) === arr.lastIndexOf(num));
};
Now you have to run your tests:
describe('Actions.js', () => {
it('verify no duplicate action values', () => {
const actionsArray = getDeepValues(actions);
const hasNoDuplicates = arrayHasNoDuplicate(actionsArray);
expect(hasNoDuplicates).toBeTruthy();
})
})
Hope it helps!
A unit test of this complexity probably merits its own matcher, which you could define recursively like this:
expect.extend({
toHaveUniqueValues(received) {
const keys = []
const values = new Set()
function assertUniqueValues(object) {
if (typeof object === 'object' && object !== null) {
for (const key in object) {
if (object.hasOwnProperty(key)) {
keys.push(key)
assertUniqueValues(object[key])
keys.pop()
}
}
} else if (values.has(object)) {
throw new Error(`expected ${keys.join('.')} to not have duplicate value ${String(object)}`)
}
values.add(object)
}
try {
assertUniqueValues(received)
return {
message: () => 'expected object to have duplicate values',
pass: true
}
} catch (error) {
return {
message: () => error.message,
pass: false
}
}
}
})
The message that goes with pass: true, as explained in the documentation, is in case the test is negated and the negation of the test fails. Set is preferred to Array for storing the values found so far, because lookup using has() is O(1) time on average while using includes() is O(n) time.
To use the above matcher in your case:
describe('actions', () => {
it('should not have duplicate values', () => {
expect(actions).toHaveUniqueValues()
})
})
In this case it will complete with the error:
expected OUTSIDE.UPDATE_PROFILE_ID to not have duplicate value SET_PROFILE_ID

Create an Observable object from a list of Observables

I'm still wrapping my head around RxJS and there is this pattern I keep running into and that I would like to find a more elegant way to write.
Implementing the model part of a Model-View-Intent pattern component, I have a function that takes actions as input an returns a single state$ Observable as output.
function model(actions) {
const firstProperty$ =
const anotherProperty$ = …
// Better way to write this?
const state$ = Rx.Observable.combineLatest(
firstProperty$, anotherProperty$,
(firstProperty, anotherProperty) => ({
firstProperty, anotherProperty
})
);
return state$;
}
So my model method computes a bunch of observables, every one of them emit items that represents a part of the state of my application. That is fine.
But how to I cleanly combine them into a single one observable that emits states, each state being a single object whose keys are the initial observable names?
I borrowed this pattern from https://github.com/cyclejs/todomvc-cycle :
function model(initialState$, actions){
const mod$ = modifications(actions)
return initialState$
.concat(mod$)
.scan( (state, mod) => mod(state))
.share()
}
function modifications(actions){
const firstMod$ = actions.anAction$.map(anAction => (
state => ({ ...state,
firstProperty: anAction.something
})
const secondMod$ = actions.otherAction$.map(otherAction => (
state => ({ ...state,
firstProperty: otherAction.something,
secondProperty: aComputation(otherAction)
})
return Rx.Observable.merge([firstMod$, secondMod$ ]).share()
}
In the main function :
const initialState$ = Rx.Observable.from({})
const actions = intent(DOM)
const state$ = model(initialState$, actions).share()
Using help from CHadrien, here is a working solution.
const prop1$ = Rx.Observable.of('foo');
const prop2$ = Rx.Observable.of('bar');
const prop3$ = Rx.Observable.of('baz');
const prop4$ = Rx.Observable.of('foobar');
function combineObservables(objectOfObservables) {
const keys = Object.keys(objectOfObservables);
const observables = keys.map(key => objectOfObservables[key]);
const combined$ = Rx.Observable.combineLatest(
observables, (...values) => {
var obj = {};
for (let i = 0 ; i < keys.length ; i++) {
obj[keys[i]] = values[i];
}
return obj;
}
);
return combined$;
}
combineObservables({prop1$, prop2$, prop3$, prop4$}).subscribe(x => console.log(x));
And the result:
[object Object] {
prop1$: "foo",
prop2$: "bar",
prop3$: "baz",
prop4$: "foobar"
}

Categories