How to stop re-rendering a whole list of items when only one item of the list is created or updated in ReactJs React-Redux? - javascript

I am making this web applications which has posts where users can put answers to those posts. I have used React-Redux to manage the state of the application. Every time I create or update an answer of a particular post the whole list of answers which belongs to that post gets re-rendered and I want to stop that and render only the newly created or updated one. I have used exactly the same way for post comments and it works fine. Comments doesn't get re-rendered but answers does. I just can't figure out what is the problem here. Please refer the code below.
I tried using React.memo() also and it doesn't work either!
Answer render component,
export function Answer() {
const classes = useStyles();
const dispatch = useDispatch();
const { postId } = useParams();
const postAnswers = useSelector(state => state.Answers);
const [answers, setAnswers] = React.useState(postAnswers.answers);
React.useEffect(() => {
if(postAnswers.status === 'idle') dispatch(fetchAnswers(postId));
}, [dispatch]);
React.useEffect(() => {
if(postAnswers.answers) handleAnswers(postAnswers.answers);
}, [postAnswers]);
const handleAnswers = (answers) => {
setAnswers(answers);
};
const AnswersList = answers ? answers.map(item => {
const displayContent = item.answerContent;
return(
<Grid item key={item.id}>
<Grid container direction="column">
<Grid item>
<Paper component="form" className={classes.root} elevation={0} variant="outlined" >
<div className={classes.input}>
<Typography>{displayContent}</Typography>
</div>
</Paper>
</Grid>
</Grid>
</Grid>
);
}): undefined;
return(
<Grid container direction="column" spacing={2}>
<Grid item>
<Divider/>
</Grid>
<Grid item>
<Grid container direction="column" alignItems="flex-start" justify="center" spacing={2}>
{AnswersList}
</Grid>
</Grid>
<Grid item>
<Divider/>
</Grid>
</Grid>
);
}
Fetch answers redux apply,
export const fetchAnswers = (postId) => (dispatch) => {
dispatch(answersLoading());
axios.get(baseUrl + `/answer_api/?postBelong=${postId}`)
.then(answers =>
dispatch(addAnswers(answers.data))
)
.catch(error => {
console.log(error);
dispatch(answersFailed(error));
});
}
Post answers,
export const postAnswer = (data) => (dispatch) => {
axios.post(baseUrl + `/answer_api/answer/create/`,
data
)
.then(response => {
console.log(response);
dispatch(fetchAnswers(postBelong)); //This is the way that I update answers state every time a new answer is created or updated
})
.catch(error => {
console.log(error);
});
}
Any help would be great. Thank you!

After adding an item you fetch all the items from the api so all items are recreated in the state. If you give a container component the id of the item and have the selector get the item as JSON then parse back to object you can memoize it and prevent re render but I think it's probably better to just re render.
Here is an example of memoized JSON for the item:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const fakeApi = (() => {
const id = ((num) => () => ++num)(1);
const items = [{ id: 1 }];
const addItem = () =>
Promise.resolve().then(() =>
items.push({
id: id(),
})
);
const updateFirst = () =>
Promise.resolve().then(() => {
items[0] = { ...items[0], updated: id() };
});
const getItems = () =>
//this is what getting all the items from api
// would do, it re creates all the items
Promise.resolve(JSON.parse(JSON.stringify(items)));
return {
addItem,
getItems,
updateFirst,
};
})();
const initialState = {
items: [],
};
//action types
const GET_ITEMS_SUCCESS = 'GET_ITEMS_SUCCESS';
//action creators
const getItemsSuccess = (items) => ({
type: GET_ITEMS_SUCCESS,
payload: items,
});
const getItems = () => (dispatch) =>
fakeApi
.getItems()
.then((items) => dispatch(getItemsSuccess(items)));
const update = () => (dispatch) =>
fakeApi.updateFirst().then(() => getItems()(dispatch));
const addItem = () => (dispatch) =>
fakeApi.addItem().then(() => getItems()(dispatch));
const reducer = (state, { type, payload }) => {
if (type === GET_ITEMS_SUCCESS) {
return { ...state, items: payload };
}
return state;
};
//selectors
const selectItems = (state) => state.items;
const selectItemById = createSelector(
[selectItems, (_, id) => id],
(items, id) => items.find((item) => item.id === id)
);
const createSelectItemAsJSON = (id) =>
createSelector(
[(state) => selectItemById(state, id)],
//return the item as primitive (string)
(item) => JSON.stringify(item)
);
const createSelectItemById = (id) =>
createSelector(
[createSelectItemAsJSON(id)],
//return the json item as object
(item) => JSON.parse(item)
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
//simple thunk implementation
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const Item = React.memo(function Item({ item }) {
const rendered = React.useRef(0);
rendered.current++;
return (
<li>
rendered:{rendered.current} times, item:{' '}
{JSON.stringify(item)}
</li>
);
});
const ItemContainer = ({ id }) => {
const selectItem = React.useMemo(
() => createSelectItemById(id),
[id]
);
const item = useSelector(selectItem);
return <Item item={item} />;
};
const ItemList = () => {
const items = useSelector(selectItems);
return (
<ul>
{items.map(({ id }) => (
<ItemContainer key={id} id={id} />
))}
</ul>
);
};
const App = () => {
const dispatch = useDispatch();
React.useEffect(() => dispatch(getItems()), [dispatch]);
return (
<div>
<button onClick={() => dispatch(addItem())}>
add item
</button>
<button onClick={() => dispatch(update())}>
update first item
</button>
<ItemList />
</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 just found where the problem was which led to the above question.
In my state management system there is an action named answers to handle the state of post answers like below.
import * as ActionTypes from '../ActionTypes';
export const Answers = (state = {
status: 'idle',
errMess: null,
answers: []
}, action) => {
switch(action.type) {
case ActionTypes.ADD_ANSWER_LIST:
return {...state, status: 'succeeded', errMess: null, answers: action.payload}
case ActionTypes.ANSWER_LIST_LOADING:
return {...state, status: 'loading', errMess: null, answers: []}
case ActionTypes.ANSWER_LIST_FAILED:
return {...state, status: 'failed', errMess: action.payload, answers: []}
default:
return state;
}
}
The problem here is that the empty arrays that I have put in ANSWER_LIST_LOADING and ANSWER_LIST_FAILED cases. Every time the action creator fetches new data, it goes through the loading state and there it gets an empty array which leads the whole list of answers to be re-rendered and re-created unnecessarily. So I changed the implementation as follows and it fixed the problem.
export const Answers = (state = {
status: 'idle',
errMess: null,
answers: []
}, action) => {
switch(action.type) {
case ActionTypes.ADD_ANSWER_LIST:
return {...state, status: 'succeeded', errMess: null, answers: action.payload}
case ActionTypes.ANSWER_LIST_LOADING:
return {...state, status: 'loading', errMess: null, answers: [...state.answers]}
case ActionTypes.ANSWER_LIST_FAILED:
return {...state, status: 'failed', errMess: action.payload, answers: [...state.answers]}
default:
return state;
}
}
All the time the problem has been in a place where I never thought it would be. I haven't even mentioned about this action in my question. But there you go.

Related

Send order to children

const Parent = ({list}) => {
const closeAll = () => {
// What should be in here?
}
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map(item => <Accordion item={item}/>)}
</>
)
}
const Accordion = ({item}) => {
const [open, setOpen] = useState(false);
return (
<div onClick={() => setOpen(o => !o)}>
<p>item.name</p>
{open && <p>item.detail</p>
</div>
)
}
Basically, as above, there is the Accordion components and a parent component that hosts all of them. Each Accordion component has a state called open. I want to change state of each child from parent component. How can I send an order to a child component to change its state?
Lift your state up into Parent.
closeAll can just map over the list and set all the open properties to false.
Have a handleClick callback that you pass down to Accordion which sets the state of the clicked item's open property to the inverse in Parent
Take a look at the react docs for lifting state up.
import { useState } from "react";
const data = [
{
detail: "foo",
name: "bar"
},
{
detail: "foo1",
name: "bar1"
}
];
const Parent = ({ defaultList = data }) => {
const [list, setList] = useState(
defaultList.map((i) => ({
...i,
open: false
}))
);
const closeAll = () => {
setList(
list.map((i) => ({
...i,
open: false
}))
);
};
const handleClick = (i) => {
const newList = [...list];
newList[i].open = !list[i].open;
setList(newList);
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} handleClick={() => handleClick(i)} />
))}
</>
);
};
const Accordion = ({ item, handleClick }) => {
return (
<div>
<button onClick={handleClick}>{item.name}</button>
{item.open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;
If you are unable to lift your state there is an alternative approach using react refs.
Create ref (initially an empty array) that each Accordion will push its own close state setting function into when it first renders.
In Parent, loop over the the array of close state settings functions inside the ref and execute each.
const Parent = ({ list = data }) => {
const myRef = useRef([]);
const closeAll = () => {
myRef.current.forEach((c) => c());
};
return (
<>
<button onClick={() => closeAll()}>Close All</button>
{list.map((item, i) => (
<Accordion item={item} myRef={myRef} />
))}
</>
);
};
const Accordion = ({ item, myRef }) => {
const [open, setOpen] = useState(false);
useEffect(() => {
myRef.current.push(() => setOpen(false));
}, [myRef]);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>{item.name}</button>
{open && <p>{item.detail}</p>}
</div>
);
};
export default Parent;
Using an internal state for the component is not recommended, at least from my point of view for what you are doing.
you can control the open state of each list item from its properties like the example here:
const Parent = ({ list }) => {
const [isAllClosed, setIsAllClosed] = useState(false);
const closeAll = () => {
setIsAllClosed(true)
};
return (
<>
<button onClick={closeAll}>Close All</button>
{list.map((item) => (
item.open = isAllClosed != null ? (!isAllClosed) : true;
<Accordion item={item} />
))}
</>
);
};
const Accordion = ({ item }) => {
return (
<div onClick={() => console.log('item clicked')}>
<p>item.name</p>
{item.open ? <p>item.detail</p> : null}
</div>
);
};
I also replaced you short circuit evaluation open && <p>item.detail</p> to a ternary. The reason for that is that you will get a string false being printed if not true, does it make sense?
You will need somehow control the state of the whole list whether an item is open or not from whoever is using the parent.
But avoid using internal state when you can.
I think you can try creating a state variable inside the parent and passing it as a prop to the child to control the behavior of the child.
const Parent = ({ list }) => {
const [masterOpen, setMasterOpen] = useState(true);
<>
<button onClick={() => setMasterOpen(false)}>Close All</button>
{list.map((item) => (
<Accordion item={item} parentOpen={masterOpen} />
))}
</>
);
};
const Accordion = ({ item, parentOpen }) => {
const [open, setOpen] = useState(false);
if (!parentOpen) {
setOpen(false);
}
return (
<div onClick={() => setOpen((o) => !o)}>
<p>{item.name}</p>
{open && <p>item.detail</p>}
</div>
);
};

Why doesn't my component re-render when state changes (redux)?

I'm learning redux and i'm making a sort of pokedex app where i fetch 20 pokemons from pokeapi.co at a time. When the page changes a new list of 20 pokemons is fetched. The problem is that while state changes to the new pokemons, they don't actually render.
App.js
const App = () => {
const dispatch = useDispatch();
const offset = useSelector(state => state.offset);
const limit = useSelector(state => state.limit);
useEffect(() => {
//FETCHES 20 OBJECTS THAT CONTAIN AN URL TO AN INDIVIDUAL POKEMON
dispatch(fetchPokemons(limit, offset));
}, [limit, offset, dispatch]);
...
return (
<div style={{ backgroundColor: '#222222' }}>
<Notification />
<AppBarPokemon />
<Switch>
<Route path="/pokemons">
<PokemonsDisplay CapsFirstLetter={CapsFirstLetter}/>
</Route>
...
PokemonsDisplay.js
const PokemonsDisplay = ({ CapsFirstLetter }) => {
const dispatch = useDispatch();
const classes = useStyles();
const pokemons = useSelector(state => state.pokemons);
console.log(pokemons);
const pageSize = 20;
const totalCount = 898;
const handleClick = (p) => {
dispatch(getOnePokemon(p));
};
return (
<div className={classes.root}>
{pokemons && (
<Grid container spacing={3}>
{pokemons.map(p => (
<Grid item xs={3} key={p.name} className={classes.gridItem} component={Link} onClick={() => handleClick(p)} to={`/pokemons/${p.name}`} data-cy={`pokemon-button-${p.name}`}>
<Paper className={classes.paper && classes.color} elevation={10}>
<p className={classes.p}>#{p.id}</p>
<p className={classes.p}>{CapsFirstLetter(p.name)}</p>
{p.sprites.other["dream_world"]["front_default"] !== null ?
<img className={classes.image} alt={`${p.name}'s sprite`} src={p.sprites.other["dream_world"]["front_default"]}/> :
<img className={classes.image} alt={`${p.name}'s sprite`} src={p.sprites.other["official-artwork"]["front_default"]}/>}
</Paper>
</Grid>
))}
</Grid>
)}
<Pagination
totalCount={totalCount}
pageSize={pageSize}
/>
</div>
);
};
pokemonsReducer.js
import getPokemons from '../services/pokemons';
import axios from 'axios';
import { loadPokemonsFromLS, savePokemonsList } from '../utils/localStoragePokemons';
const pokemonsReducer = (state = [], action) => {
console.log('state is:', state)
switch(action.type){
case 'INIT_POKEMONS':
return action.data;
default:
return state;
};
};
export const fetchPokemons = (limit, offset) => {
return async dispatch => {
try {
const pokemons = loadPokemonsFromLS(limit, offset);
dispatch({
type: 'INIT_POKEMONS',
data: pokemons
});
} catch (error) {
const pokemons = await getPokemons.getPokemons(limit, offset);
let pokemonsArray = [];
let pokemonsObject = {};
pokemons.results.forEach(async (r, i) => {
//FETCHES EACH POKEMON URL AND STORES ITS DATA ON pokemons STATE
const pokemonNow = await axios.get(r.url);
pokemonsArray.push(pokemonNow.data);
pokemonsObject[i] = pokemonNow.data
});
savePokemonsList(limit, offset, pokemonsObject);
dispatch({
type: 'INIT_POKEMONS',
data: pokemonsArray
});
}
};
};
export default pokemonsReducer;
I have tried to dispatch({ data: [...pokemons] })
But it doesnt work.
Also i forgot to add. When i go to my component that is routed to '/' and then back to '/pokemons' they render.
Edit: I think i'm getting there.
i changed the reducer function so that it gets called independently from the dispatch, the problem is that now the action doesn't get fired xD.
export const fetchEverything = async (limit, offset) => {
try {
const pokemons = loadPokemonsFromLS(limit, offset);
initPokemons(pokemons);
} catch (error) {
const pokemonsData = await getPokemons.getPokemons(limit, offset);
let pokemons = [];
let pokemonsObject = {};
console.log(pokemonsData)
pokemonsData.results.forEach(async (r, i) => {
//FETCHES EACH POKEMON URL AND STORES ITS DATA ON pokemons STATE
const pokemonNow = await axios.get(r.url);
pokemonsObject[i] = pokemonNow.data;
//console.log([pokemonNow.data][0]);
pokemons.push(pokemonNow.data);
});
console.log(pokemons);
console.log(pokemonsObject);
savePokemonsList(limit, offset, pokemonsObject);
initPokemons(pokemons);
};
};
export const initPokemons = (pokemons) => {
return dispatch => dispatch({ type: 'INIT_POKEMONS', pokemons: pokemons })
};
const pokemonsReducer = (state = [], action) => {
switch(action.type){
case 'INIT_POKEMONS':
console.log(action);
const newState = action.pokemons
return newState;
default:
return state;
};
};
It happens due to redux state mutation, you can resolve this issue using immer as stated in redux documentation as well. https://www.npmjs.com/package/immer

remove an item from the array via an action

I want to make remove item reducer
add item reducer is :
export const addItems= (state= [], action)=> {
switch (action.type) {
case 'ADD_ITEM':
return [
...state,
action.product
]
default:
return state;
};
};
action creator for add itme is :
export const showItems = (author,price) =>{
return((dispatch)=>{
dispatch({
type:'ADD_ITEM',
product:{
author,
price
}
});
});
};
remove action creator is :
export const removeItem = (index) =>{
return((dispatch)=>{
dispatch({
type:'REMOVE_ITEM',
payload: index
});
});
};
the map function that show list item :
{showItems.map((item, index)=>{
return(
<ul key={index} className='d-flex justify-content-around'>
<button
type='button'
className='btn-close btn-danger'
/>
<p>{item.author}</p>
<p>{item.price}</p>
</ul>
);
})}
my question is : what is remove item reducer?
If you remove items from a list you should not use index as a key, you can have your reducer create a unique index for you if the data is not stored on the server and accessed by multiple clients.
To remove an item from the array you can use Array.prototype.filter:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
items: [{ id: 1 }, { id: 2 }],
};
//action types
const ADD = 'ADD';
const REMOVE = 'REMOVE';
//action creators
// no need for remove to be a thunk
const remove = (id) => ({
type: 'REMOVE',
payload: id,
});
const add = () => ({ type: ADD });
//function to generate unique id based on state
const getId = (state) =>
Math.max(
...(state.items.length
? state.items.map(({ id }) => id)
: [1])
) + 1;
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return {
...state,
items: state.items.concat({
id: getId(state),
}),
};
}
if (type === REMOVE) {
//use Array.prototype.filter to remove item from array
return {
...state,
items: state.items.filter(({ id }) => id !== payload),
};
}
return state;
};
//selectors
const selectItems = (state) => state.items;
const createSelectItemById = (itemId) =>
createSelector([selectItems], (items) =>
items.find(({ id }) => id === itemId)
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
const Item = React.memo(function Item({ id }) {
const selectItem = React.useMemo(
() => createSelectItemById(id),
[id]
);
const item = useSelector(selectItem);
const dispatch = useDispatch();
return (
<li>
<pre>{JSON.stringify(item)}</pre>
<button onClick={() => dispatch(remove(id))}>
remove
</button>
</li>
);
});
const App = () => {
const items = useSelector(selectItems);
const dispatch = useDispatch();
return (
<div>
<button onClick={() => dispatch(add())}>
Add Item
</button>
<ul>
{items.map(({ id }) => (
// use unique id for key when you re order
// or remove items
<Item key={id} id={id} />
))}
</ul>
</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>
For more immutable update patterns see this article.

React: is this a good way to implement a shared state subscription?

Not sure if this is a so-called "pub/sub" pattern or a form of a "pub/sub" pattern. I am trying to create a piece of shared state so that different components can subscribe to it and only gets updated when there is an update with that state.
const useForceUpdate = () => useReducer((state) => !state, false)[1];
const createSharedState = (reducer, initialState) => {
const subscribers = [];
let state = initialState;
const dispatch = (action) => {
state = reducer(state, action);
subscribers.forEach((callback) => callback());
};
const useSharedState = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const callback = () => forceUpdate();
subscribers.push(callback);
const cleanup = () => {
const index = subscribers.indexOf(callback);
subscribers.splice(index, 1);
};
return cleanup;
}, []);
return [state, dispatch];
};
return useSharedState;
};
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "set":
return action.count;
default:
return state;
}
};
const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
const Counter = ({ count, dispatch }) => (
<div>
{count}
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "set", count: 0 })}>reset</button>
</div>
);
const Counter1 = () => {
const [count, dispatch] = useCount1();
return <Counter count={count} dispatch={dispatch} />;
};
const Counter2 = () => {
const [count, dispatch] = useCount2();
return <Counter count={count} dispatch={dispatch} />;
};
const Example = () => (
<>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</>
);
<script src="https://unpkg.com/#babel/standalone#7/babel.min.js"></script>
<script src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="root"></div>
<script type="text/babel">
const { useEffect, useReducer } = React;
const useForceUpdate = () => useReducer((state) => !state, false)[1];
const createSharedState = (reducer, initialState) => {
const subscribers = [];
let state = initialState;
const dispatch = (action) => {
state = reducer(state, action);
subscribers.forEach((callback) => callback());
};
const useSharedState = () => {
const forceUpdate = useForceUpdate();
useEffect(() => {
const callback = () => forceUpdate();
subscribers.push(callback);
const cleanup = () => {
const index = subscribers.indexOf(callback);
subscribers.splice(index, 1);
};
return cleanup;
}, []);
return [state, dispatch];
};
return useSharedState;
};
const initialState = 0;
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return state + 1;
case "decrement":
return state - 1;
case "set":
return action.count;
default:
return state;
}
};
const useCount1 = createSharedState(reducer, initialState);
const useCount2 = createSharedState(reducer, initialState);
const Counter = ({ count, dispatch }) => (
<div>
{count}
<button onClick={() => dispatch({ type: "increment" })}>+1</button>
<button onClick={() => dispatch({ type: "decrement" })}>-1</button>
<button onClick={() => dispatch({ type: "set", count: 0 })}>reset</button>
</div>
);
const Counter1 = () => {
const [count, dispatch] = useCount1();
return <Counter count={count} dispatch={dispatch} />;
};
const Counter2 = () => {
const [count, dispatch] = useCount2();
return <Counter count={count} dispatch={dispatch} />;
};
const Example = () => (
<>
<Counter1 />
<Counter1 />
<Counter2 />
<Counter2 />
</>
);
ReactDOM.render(<Example />, document.querySelector("#root"));
</script>
It seems to be working fine. My questions are:
Is this a valid way to implement shared update subscription?
Is there any drawbacks with using a simple variable to hold the state + forcing React to re-render if that piece of state changes, instead of using useState or useReducer as one normally would do?
any feedback is welcomed.
Your idea is excellent. React team was also thinking on this topic and ended up with the creation of https://recoiljs.org/. You can use it as useState (DEMO) or as useReducer (DEMO).
I don't want to highlight your solution's drawbacks. Instead, I'd like to list the advantages of using Recoil:
Internal memory usage optimization.
No need to support the code (Facebook does it).
No cheating (useForceUpdate).
Supports selectors out of the box.
I'd recommend you to learn more about Recoil and start using it because it gives the exact result you want to achieve.

How can I repeatedly filter an array?

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.

Categories

Resources