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}
Related
I am using React Router Dom v6. I would like to store some object search parameters in the URL. Right now, I am basically doing something like this:
const [searchParams, setSearchParams] = useSearchParams();
const allSearchParams = useMemo(() => {
const params: Record<string, string> = {};
for (const [key, value] of searchParams.entries()) {
if (value.startsWith('{') || value.startsWith('[')) {
try {
params[key] = JSON.parse(value);
} catch {
params[key] = value;
}
} else {
params[key] = value;
}
}
return params;
}, [searchParams]);
And when writing in the URL, I do:
const newF: Record<string, string> = { ...nextFilter };
Object.keys(newF).forEach((key) => {
if (typeof newF[key] === 'object' && newF[key]) {
newF[key] = JSON.stringify(newF[key]);
}
});
setSearchParams(createSearchParams(newF), {
replace: replaceSearch,
});
But this feels pretty hacky. Is there a proper way to store objects in the URL properly and safely? For example:
const filters = {
name: "user1",
region: {
city: "Sydney",
country: "Australia"
}
}
You can simplify your encoding and decoding process. You can use the below functions; you can add them in their own file and import them:
import {createSearchParams} from "react-router-dom"
export const encodeSearchParams = (params) => createSearchParams(params);
export const decodeSearchParams = (searchParams) => {
return [...searchParams.entries()].reduce((acc, [key, val]) => {
try {
return {
...acc,
[key]: JSON.parse(val)
};
} catch {
return {
...acc,
[key]: val
};
}
}, {});
};
This way, you don't have to memoize them. And you can use them like below, for example:
function HomePage() {
const [searchParams, setSearchParams] = useSearchParams();
function handleQueryParamsChange() {
const filters = {
name: "user1",
region: {
city: "Sydney",
country: "Australia"
}
};
const params = {
filters: JSON.stringify(filters),
anotherField: "Simple String"
};
setSearchParams(encodeSearchParams(params));
}
console.log(decodeSearchParams(searchParams).filters);
console.log(decodeSearchParams(searchParams).anotherField);
return (
<div>
<button onClick={handleQueryParamsChange} className="page">
Update query params
</button>
</div>
);
}
A video with the behavior, I am clicking one time in 'A' and one time 'D', alternatively and is like there is two states, really strange!
https://www.loom.com/share/ba7a97f008b14529b15dca5396174c8c
And here is the action to update the description!
if (action.type === 'description') {
const { payload } = action;
const { description } = payload;
const objIndex = state.findIndex(obj => obj === payload.state);
state[objIndex].description = description;
return [...state];
}
And this is the big picture as requested, I tried to simplify to the code that I am testing in description input:
//outside component
const reducer = (state, action) => {
if (action.type === 'initialState') {
const { payload } = action;
console.log('state', state);
console.log('payload', payload);
return state.concat(payload);
}
if (action.type === 'description') {
const { payload } = action;
const { description } = payload;
const objIndex = state.findIndex(obj => obj === payload.state);
state[objIndex].description = description;
return [...state];
}
};
//inside component
const [states, dispatch] = useReducer(reducer, []);
function updateInitialState(value) {
dispatch({ type: 'initialState', payload: value });
}
function updateDescription(payload) {
dispatch({ type: 'description', payload });
}
useEffect(() => {
states.forEach(state => {
const descriptionInput = (state.status === undefined || state.status === 'available') && (
<FormInput
name="description"
label="Descrição"
input={
<InputText
value={state.description || ''}
onChange={({ target: { value } }) => {
const payload = { description: value, state };
updateDescription(payload);
}}
placeholder="Descrição"
/>
}
/>
);
const index = states.findIndex(e => e === state);
const updateArray = arrayInputs;
updateArray[index] = [descriptionInput];
setArrayInputs(updateArray);
});
}, [states]);
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)
So, now i'm making to-do-list, and i have problems with buttons 'active' and 'done' tasks. When i press one of these button, it has to return tasks which are done/active, and it returns, but only 1 time. I guess it makes a new array, and delete old array. So how to make filter, which won't delete my array and just filter tasks which are done or active? And every time I click on these buttons, I will be shown tasks filtered on done/active/all.
P.S. sorry for ENG
onst ADD_TASK = 'ADD_TASK'
const EDIT_STATUS = 'EDIT_STATUS'
const TASK_DELETE = 'TASK_DELETE'
const DONE_TASK = 'DONE_TASK'
const ACTIVE_TASKS = 'ACTIVE_TASKS'
const initialState = {
tasks: []
};
const mainReducer = (state = initialState, action) => {
switch (action.type) {
case ADD_TASK: {
return {
...state,
tasks: [{
id: shortid.generate(),
task: action.task,
status: false
}, ...state.tasks], filter: 'all'
}
}
case EDIT_STATUS: {
return {
...state,
tasks: state.tasks.map(task => task.id === action.id ? {...task, status: !task.status} : task)
}
}
case TASK_DELETE: {
return {
...state,
tasks: state.tasks.filter(t => t.id !== action.id)
}
}
case DONE_TASK: {
return {
...state,
tasks: state.tasks.filter(t => !t.status),
filter: 'done'
}
return state.tasks
}
case ACTIVE_TASKS: {
return {
...state,
tasks: state.tasks.filter(t => t.status),
filter: 'active'
}
return state.tasks
}
default:
return state
}
}
export const doneTask = () => ({type: 'DONE_TASK'})
export const activeTask = () => ({type: 'ACTIVE_TASKS'})
export const addTask = task => ({type: 'ADD_TASK', task});
export const editStatus = id => ({type: 'EDIT_STATUS', id})
export const deleteTask = id => ({type: 'TASK_DELETE', id})
export default mainReducer;
Here is an example of how to store local state and pass it to ConnectedList as props.done.
ConnectedList has selectFilteredTasks as mapStateToProps and that is a selector created with reselect to get tasks, the second argument to this function is props so if props.done is not undefined it'll filter out the tasks that are done.
const { useState } = React;
const {
Provider,
connect,
} = ReactRedux;
const { createStore } = Redux;
const { createSelector } = Reselect;
const state = {
tasks: [
{
id: 1,
task: 'one',
status: false,
},
{
id: 2,
task: 'two',
status: true,
},
],
};
const store = createStore(
(x) => x, //won't dispatch any actions
{ ...state },
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__()
);
//selectors
const selectTasks = (state) => state.tasks;
const selectFilteredTasks = createSelector(
selectTasks,
(_, { done }) => done, //get the second argument passed to selectFilteredTasks
(tasks, done) =>
done !== undefined
? {
tasks: tasks.filter(
(task) => task.status === done
),
}
: { tasks }
);
const List = ({ tasks }) => (
<ul>
{tasks.map((task) => (
<li key={task.id}>
<pre>{JSON.stringify(task)}</pre>
</li>
))}
</ul>
);
const ConnectedList = connect(selectFilteredTasks)(List);
const App = () => {
const [done, setDone] = useState();
return (
<div>
<label>
only done
<input
type="checkbox"
onClick={() => setDone(done ? undefined : true)}
></input>
</label>
<ConnectedList done={done} />
</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>
I suggest you to go with different approach.
In button click function, you can get all todos and return filtered out todos which are active/completed instead of performing operation on reducer.
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 }