I'm currently trying to take two objects of objects, where the second one has updated values, and merge the updated values into the first one. I wrote a function to do this but i'm unable to update the values within my AnimatedDataWrapper. However if I run it outside of the AnimatedDataWrapper it works fine..
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import * as d3 from 'd3'
const mapNewStateToOldState = (oldState, newState) => {
Object.keys(oldState).forEach((key) => {
Object.assign(oldState[key], newState[key])
})
return oldState
}
// const mapNewStateToOldState = (oldState, newState) =>
// Object.keys(oldState).map(key => Object.assign(oldState[key], newState[key]))
const obj = { 0: { data: 1 } }
const newObj = { 0: { data: 2 } }
console.log(mapNewStateToOldState(obj, newObj)) // THIS WORKS
console.log(obj) // THIS WORKS
const AnimatedDataWrapper = (dataProp, transitionDuration = 300) => ComposedComponent =>
class extends Component {
constructor(props) {
super(props)
const data = this.props[dataProp]
this.state = Object.keys(data)
.map(label => ({ [label]: data[label] }))
.reduce((prev, curr) => ({ ...prev, ...curr }), {})
}
componentWillReceiveProps(nextProps) {
const data = this.props[dataProp]
console.log(data)
const nextData = nextProps[dataProp]
const dataKeys = this.props.dataKeys
const dataUnchanged = Object.keys(data)
.map(label => data[label] === nextData[label])
.reduce((prev, curr) => prev && curr)
if (dataUnchanged) {
return
}
d3.select(this).transition().tween('attr.scale', null)
d3
.select(this)
.transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.tween('attr.scale', () => {
const barInterpolators = data.map((...args) => {
const index = args[1]
return dataKeys.map((key) => {
const interpolator = d3.interpolateNumber(
this.state[index][key],
nextData[index][key],
)
return { key, interpolator }
})
})
return (t) => {
const newState = barInterpolators
.map(bar =>
bar
.map(({ key, interpolator }) => ({ [key]: interpolator(t) }))
.reduce((result, currentObject) => {
Object.keys(currentObject).map((key) => {
if (Object.prototype.hasOwnProperty.call(currentObject, key)) {
result[key] = currentObject[key]
}
return null
})
return result
}, {}),
)
.reduce((newObject, value, index) => {
newObject[index] = value
return newObject
}, {})
const oldState = this.state
console.log(`OLD STATE = ${JSON.stringify(oldState)}`)
console.log(`NEW STATE = ${JSON.stringify(newState)}`)
const updatedState = mapNewStateToOldState(oldState, newState) // THIS DOES NOT WORK
console.log(`UPDATED STATE = ${JSON.stringify(updatedState)}`)
this.setState(updatedState)
}
})
}
render() {
const { props, state } = this
const newData = Object.keys(state).map(val => state[val])
const newDataProps = { ...{ data: newData } }
const newProps = { ...props, ...newDataProps }
return <ComposedComponent {...newProps} />
}
}
AnimatedDataWrapper.PropType = {
dataProp: PropTypes.string.isRequired,
transitionDuration: PropTypes.number,
dataKeys: PropTypes.instanceOf(Array).isRequired,
maxSurf: PropTypes.number.isRequired,
}
export default AnimatedDataWrapper
Here is what the objects i'm passing into the function mapNewStateToOldState (oldState, newState) look like. And what the output updatedState looks like.
It seems like maybe it would be a scoping issue? But i can't seem to figure out what is going on. I tried manually merging it with no luck either.
Good ol' Object.assign will do the job you're looking for, where preceding objects will be overwritten by others that follow with the same keys:
var oldState = {a: 1, b: 2}
var newState = {b: 3, c: 4}
Object.assign(oldState, newState) === { a: 1, b: 3, c: 4 }
In stage-3 ecmascript you can use the spread syntax:
var oldState = {a: 1, b: 2}
var newState = {b: 3, c: 4}
{ ...oldState, ...newState } === { a: 1, b: 3, c: 4 }
Related
am fairly new to React/Redux. I have a normalised redux state that looks like so:
users: {
user1: {
id: 'user1',
name: 'Jim',
carts: ['cart1', 'cart2']
}
},
carts: {
cart1: {
id: 'cart1',
items: ['item1', 'item2']
},
cart2: {
id: 'cart2',
items: ['item3', 'item4']
}
},
items: {
item1: {
id: 'item1',
price: 1
},
item2: {
id: 'item2',
price: 2
},
item3: {
id: 'item3',
price: 3
},
item4: {
id: 'item4',
price: 4
}
}
I have also created the following selectors:
item.selector.js:
export const selectAllItems = state => state.items
export const selectItems = itemIds => createSelector(
[selectAllItems],
items => Object.keys(items)
.filter(itemId => itemIds.includes(itemId))
.reduce((arr, itemId) => {
arr.push(items[itemId])
return arr
}, [])
)
export const totalItemsCost = itemIds => createSelector(
[selectAllItems],
items => Object.keys(items)
.filter(itemId => itemIds.includes(itemId))
.reduce((cost, itemId) => {
cost += blocs[itemId].price
return cost
}, 0)
)
cart.selector.js:
export const selectAllCarts = state => state.carts
export const selectCart = cartId => createSelector(
[selectAllCarts],
carts => carts[cartId]
)
export const selectCarts = cartIds => createSelector(
[selectAllCarts],
options => Object.keys(carts)
.filter(key => cartIds.includes(key))
.reduce((arr, key) => {
arr.push(options[key])
return arr
}, [])
)
user.selector.js:
export const selectUsers = state => state.users
export const selectUser = userId => createSelector(
[selectUsers],
users => users[userId]
)
I wish to display the total cost of each cart on the user page and have the following React component:
class User extends React.Component {
render () {
return (
{ this.props.carts.map(cart => (<DisplayComponent id={ cart.id } cost={ $TOTAL_CART_COST }/>))}
)
}
}
const mapStateToProps = (state, ownProps) => ({
user: selectUser(ownProps.match.params.userId)(state),
carts: selectCarts(selectUser(ownProps.match.params.userId)(state).carts)(state)
})
export default connect(mapStateToProps, null)(User)
Im unsure on the best way to achieve this. I have tried creating a cartCost field which is updated by the reducer, like this:
case CartActionTypes.ADD_ITEM_SUCCESS:
return {
...state,
[action.payload.cartId]: {
...state[action.payload.cartId],
cartCost: totalItemsCost(addItemToCart(state[action.payload.cartId].items, action.payload.itemId))(state),
items: addItemToCart(state[action.payload.cartId].items, action.payload.itemId)
}
}
which uses:
export const addItemToCart = (items, newItem) => {
const existingItem = items.find(item => item === newItem)
if (existingItem) {
return items
} else {
return [...items, newItem]
}
}
But this gives me:
Uncaught (in promise) TypeError: Cannot convert undefined or null to object
at Function.keys (<anonymous>)
at item.selectors.js:18
at index.js:70
at index.js:30
at index.js:84
at index.js:30
at cartReducer (cart.reducer.js:35)
at combination (redux.js:459)
at persistReducer.js:147
at dispatch (redux.js:213)
at redux-logger.js:1
at index.js:11
at dispatch (redux.js:638)
at item.actions.js:30
All suggestions welcome, perhaps i'm not taking the correct approach creating the new field in the reducer? Thanks
This is what i ended up with:
cart.selector.js:
export const selectAllCarts = state => state.carts
export const selectCart = cartId => createSelector(
[selectAllCarts, selectAllItems],
(carts, items) => generateCart(carts[cartId], items)
)
export const generateCart = (cart, allItems) => {
return {
...cart,
cartCost: calculateCartCost(allItems, cart.items)
}
}
export const calculateCartCost = (allItems, cartItemIds) => {
return Object.keys(allItems)
.filter(itemId => cartItemIds.includes(itemId))
.reduce((cost, itemId) => {
cost += allItems[itemId].price
return cost
}, 0)
}
export const selectCarts = cartIds => createSelector(
[selectAllCarts, selectAllItems],
(carts, items) => Object.keys(carts)
.filter(key => cartIds.includes(key))
.reduce((arr, key) => {
arr.push(generateCart(carts[key], items))
return arr
}, [])
)
That now gives me a list of carts that all have the cart cost field i can use to display. Pretty sure its not optimal but best i have :)
Here I implemented redux like store using custom hooks. everything goes well and code executed correctly but problem is that in reducer under switch statement "TOGGLE" there I return a updated state which is finally stored in globalstate but if I returned empty object {} instead of {products: updated} still globalstate updating correctly with a change that has been done in reducer...since i am not passing globalstate reference then how it is updated correctly
and what listeners exactly do in dispatch method in code
import MarkFavMulti from "./MarkFavMulti";
import classes from "./MarkFav.module.css";
import useStore from "../HookStore/Store";
import {reducer2} from "../SampleReducer";
const MarkFav = props => {
const [outfit, dispatch] = useStore(reducer2);
const onClicked = (id) => {
dispatch({type: "TOGGLE", id: id});
}
const element = outfit.products.map((item) => {
return <MarkFavMulti cloth={item.name}
favorite={item.favorite}
price={item.price}
key={item.id}
clicked={onClicked.bind(this, item.id)} />
});
return (
<main className={classes.Markfav}>
{element}
</main>
);
};
export default MarkFav;
import {useState, useEffect} from "react";
let globalState = {};
let listeners = [];
const useStore = (reducer) => {
const setState = useState(globalState)[1];
const dispatch = (action) => {
let curr = Object.assign({},globalState);
const newState = reducer({...curr}, action)
globalState = {...globalState,...newState};
for(let listener of listeners) {
listener(globalState);
}
};
useEffect(()=>{
listeners.push(setState);
return () => {
listeners.filter(item => item !==setState);
}
},[setState]);
return [globalState, dispatch];
};
export const initStore = (initialState) => {
if(initialState) {
globalState = {...globalState, ...initialState};
}
}
export default useStore;
let initialState = {
products: [
{ id: 1, name: "shirt", price: "$12", favorite: false },
{ id: 2, name: "jeans", price: "$42", favorite: false },
{ id: 3, name: "coat", price: "$55", favorite: false },
{ id: 4, name: "shoes", price: "$8", favorite: false },
]
}
const configureStore = () => {
initStore(initialState);
};
export default configureStore;
export const reducer2 = (state=initialState, action) => {
switch (action.type) {
case "TOGGLE":
let update = {...state};
let updated = [...update.products];
updated = updated.map(item => {
if(item.id === action.id) {
item.favorite = !item.favorite;
return item;
}
return item;
});
return {products: updated};
//if we return {} ...it will updated correctly in globalstate
default:
throw new Error("not reachable");
}
}
The behavior that you are describing is due to this object assignment right here:
item.favorite = !item.favorite;
Here you are directly mutating the properties of the item object. You probably thought that it would be fine since you are using a copy of the products array.
let update = {...state};
let updated = [...update.products];
What actually happens is that updated is a "shallow copy" of the original array. The array itself is a new array, but the items in that array are the same items as in the state. You can read more about that here.
You need to return a new item object instead of mutating it. Here's a concise way to write it using the ternary operator.
case "TOGGLE":
return {
...state, // not actually necessary since products is the only property
products: state.products.map((item) =>
item.id === action.id
? {
...item,
favorite: !item.favorite
}
: item
)
};
I am converting a code from class to functional component. I am unsure as to how I would convert the code below into a hooks function.
applyFilter = () => {
const { filterLimit: value, lessThanOrGreaterThan } = this.state;
const isLessThan = lessThanOrGreaterThan === "lessThan";
// update instance variable
this.state.datas = this.state.datas.map(v => {
if (isLessThan ? v <= value : v >= value) return v;
return 0;
});
this.setState(prevState => ({
datas: this.state.datas,
}));
}
I have declared the used variables as hooks as follows-
const [filterLimit, setfilterLimit] = useState(100);
const [lessThanOrGreaterThan, setlessThanOrGreaterThan] = useState('lessThan');
const [datas, setDatas] = useState([12, 19, 3, 5, 2, 10])
Looks like you've got most of is. Looks like you just need to swap out the references to state with the props you've already defined.
const [filterLimit, setfilterLimit] = useState(100);
const [lessThanOrGreaterThan, setlessThanOrGreaterThan] = useState('lessThan');
const [datas, setDatas] = useState([12, 19, 3, 5, 2, 10])
const applyFilter = () => {
const isLessThan = lessThanOrGreaterThan === "lessThan";
// update instance variable
const newDatas = datas.map(v => {
if (isLessThan ? v <= value : v >= value) return v;
return 0;
});
setDatas(newDatas);
}
I am trying to update an array in reducer
let initialState = {
count: 0,
todos: [],
id:0,
}
const authReducer = (prevState = initialState, action) => {
switch (action.type) {
case types.ADD_TO_DO:
console.log(action.todo)
return {
...prevState,
todos: prevState.todos.concat(action.todo)
}
default:
return prevState;
}
}
And I am getting array in the form
todos:['qwerty', 'abcdef']
But I want in the form of
todos:[{id:'1', todo:'qwerty'},{id:'2',todo:'abcdef'}]
How can I achieve this?
Thanks!!!
In order to convert todos:['qwerty', 'abcdef'] to your expected format, you can map it:
var todos=['qwerty', 'abcdef'];
var result = todos.map((todo, i)=>({id:i+1, todo}));
console.log(result);
You can use reduce for this task
const todos = ['qwerty', 'abcdef']
const data = todos.reduce((acc, rec, index) => {
return [...acc, {
id: index + 1,
todo: rec
}]
}, [])
console.log(data)
I have initial state like this:
const initialState = {
array: [
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
array: [
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
}
]
},
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
},
{
key: "value",
obj: {
key1: "value",
key2: "value",
},
},
],
path: "",
value: ""
};
Reducer:
export const reducer = (state = initialState, action) => {
switch (action.type) {
case "SET_PATH":
return {
...state,
path: action.path
};
case "SET_NEW_VALUE":
return {
...state,
newValue: action.value
};
case "SET_NEW_BUILD":
//What next?
default:
return state
}
};
Action creators:
const setPath = (path) => ({type: "SET_PATH", path});
const setNewValue = (value) => ({type: "SET_NEW_VALUE", value});
const setNewBuild = (path, value) => ({type: "SET_NEW_BUILD", path, value});
And i need to change this state after this dispatch using a path string and new value.
dispatch(setNewBuild("array[0].obj.key1", "newValue");
Also the value can have form like this "obj: {key1: "newValue", key2: "newValue"}" hence will be created a new object.
How can i do this?
Here is an example using the set helper:
const REMOVE = () => REMOVE;
//helper to get state values
const get = (object, path, defaultValue) => {
const recur = (current, path, defaultValue) => {
if (current === undefined) {
return defaultValue;
}
if (path.length === 0) {
return current;
}
return recur(
current[path[0]],
path.slice(1),
defaultValue
);
};
return recur(object, path, defaultValue);
};
//helper to set state values
const set = (object, path, callback) => {
const setKey = (current, key, value) => {
if (Array.isArray(current)) {
return value === REMOVE
? current.filter((_, i) => key !== i)
: current.map((c, i) => (i === key ? value : c));
}
return value === REMOVE
? Object.entries(current).reduce((result, [k, v]) => {
if (k !== key) {
result[k] = v;
}
return result;
}, {})
: { ...current, [key]: value };
};
const recur = (current, path, newValue) => {
if (path.length === 1) {
return setKey(current, path[0], newValue);
}
return setKey(
current,
path[0],
recur(current[path[0]], path.slice(1), newValue)
);
};
const oldValue = get(object, path);
const newValue = callback(oldValue);
if (oldValue === newValue) {
return object;
}
return recur(object, path, newValue);
};
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore } = Redux;
//action
const setNewBuild = (path, value) => ({
type: 'SET_NEW_BUILD',
path,
value,
});
const initialState = {
array: [
{
key: 'value',
obj: {
key1: 'value',
key2: 'value',
},
},
],
path: '',
value: '',
};
const reducer = (state = initialState, action) => {
const { type } = action;
if (type === 'SET_NEW_BUILD') {
const { path, value } = action;
return set(state, path, () => value);
}
return state;
};
const store = createStore(
reducer,
initialState,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
const App = () => {
const state = useSelector((x) => x);
const dispatch = useDispatch();
return (
<div>
<button
onClick={() =>
dispatch(
setNewBuild(
['array', 0, 'obj', 'key1'],
'new value key 1'
)
)
}
>
change array 0 obj key1
</button>
<button
onClick={() =>
dispatch(
setNewBuild(['array', 0, 'obj'], {
key1: 'change both key1',
key2: 'change both key2',
})
)
}
>
change array 0 obj
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
The important bits are:
<button
onClick={() =>
dispatch(
// path is an array
setNewBuild(['array', 0, 'obj'], {
key1: 'change both key1',
key2: 'change both key2',
})
)
}
>
change array 0 obj
</button>
And in the reducer:
const { type } = action;
if (type === 'SET_NEW_BUILD') {
const { path, value } = action;
return set(state, path, () => value);
}
To do that with only the path there's 2 approaches, remember that in Redux everything has to be immutable, so no direct mutations should be done.
Easier approach: immer library that allows you to do mutating operations like push or using dot operator, but being immutable underneath.
Difficult approach: use spreads native JS spread operators for object and arrays, but you will understand better how things work.
I will leave the vanilla example, but if you prefer going with something straight forward you can use immer library.
function reducer(state = initialState, action) {
switch (action.type) {
case 'setNewData': {
const { array } = state;
const { path, value } = action.payload;
const index = path.match(/[\d]+/g);
const objectPath = path.split('.').slice(1);
const [elementToReplace] = array.slice(index, index + 1);
_.set(elementToReplace, objectPath, value); // using lodash set helper here
const newArray = [...array.slice(0, index), elementToReplace, ...array.slice()];
return {
...state,
array: newArray,
};
}
default: {
return state;
}
}
}
getState gives you current state at action dispatch time, also, do notice that you dispatch actions as higher-order functions ( UI-dispatch(action(payload => dispatch => dispatch({type, payload}))
//assuming { value } comes from UI and { path } refer to current stored data at state
const action = value => (dispatch, getState) =>
dispatch({type: "netNewData", value, path: getState().array[0].obj.key1}) ;
const reducer = ( state = initialState, action) => {
switch(action.type){
case "setNewData":
const { value, path } = action;
return {...state, value, path}
default: return state
}
}
This approach updates the reference as it iterates through the path keys. Those `slices are because the setter must stop at the penultimate key to set the value on the parent.
E.g. ({d: 4}).d = 6 not 4 = 6
path = ['a','b','c','d'];
state = {a: {b: {c: {d: 4}}}};
value = 6;
// jsonpath setter:
path.slice(0, -1).reduce((ref, key)=>ref[key], state)[path.slice(-1)] = value;
// jsonpath getter:
console.log(path.reduce((ref, key)=>ref[key], state), state.a.b.c); // 6 {d:6}