when using RTK Query I tried to build a remove and add to favorite feature. I'm having trouble as I can add to the state. but then the remove section, on .FindIndex() is always returning -1 Have it incorrectly used this function?
import { createSlice } from "#reduxjs/toolkit";
const initialState = {
value: false,
cardFavId: [],
};
export const favouriteSlice = createSlice({
name: "favourite",
initialState,
reducers: {
makeFav: (state, action) => {
state.value = !state.value;
state.cardFavId = [...state.cardFavId, action.payload];
},
removeFav: (state, action) => {
console.log(action.payload, "payload remove pressed");
const index = state.cardFavId.findIndex(
(cardItem) => cardItem.id === action.payload
);
let newCardFav = [...state.cardFavId];
console.log(`Index Value ${index}`);
if (index >= 0) {
//itemCard has been Faved.. remove it.
newCardFav.splice(index, 1);
} else {
// Do nothing
console.warn("Cannot remove as its not been fav ");
}
state.cardFavId = newCardFav;
},
},
});
// Action creators are generated for each case reducer function
export const { makeFav, removeFav } = favouriteSlice.actions;
export default favouriteSlice.reducer;
First of all, your findIndex callback is incorrect, because you need to compare the item's id to action.payload.id and not just the action.payload, because payload is an object.
Second, an easier way to remove an item from an array (although less efficient sometimes, but it's easier to write) is to go something like
state.cardFav = state.cardFav.filter(cardItem => cardItem.id !== action.payload.id)
Meaning, you only keep the items with an id that's different than the one you want to delete
Related
I'm using the Redux Toolkit and I'm struggling to find a way to update state within my store that also triggers a reassignment for useSelector.
const slice = createSlice({
name: "state",
initialState: [],
reducers: {
addToArray: (state, action) => {
state.push(action.payload); // This updates the store but doesn't respect immutability?
}
}
});
I'm aware the above isn't entirely correct, and that something like
state = [...state, ...action.payload]
would be better, BUT for some reason I couldn't get it work correctly any other way. I'm simply trying to add an object to the array.
My component:
export default function App() {
const array = useSelector(selectArray);
return (
{array.map((x) => {
<div>{x.text}</div>
})
)
}
The issue is, whenever the dispatch is called, array doesn't update, which I'd like it to.
I think your issue is the way you push the new value into the array. That is not immutable and it appears the selector is detecting that the array hasn't changed, so it returns the previous value.
Try this:
const slice = createSlice({
name: "state",
initialState: [],
reducers: {
addToArray: (state, action) => {
state = [ ...state, action.payload ];
}
}
});
This demo should simulate what happens when mutably changing state vs immutably changing state.
const state = {
list: [1]
}
const addItemMutable = (item) => {
const prevState = { ...state }
state.list.push(item)
// Using JSON.stringify for better readability in output.
console.log(JSON.stringify(prevState.list), JSON.stringify(state.list))
console.log(prevState.list === state.list)
}
const addItemImmutable = (item) => {
const prevState = { ...state }
state.list = [ ...state.list, item ]
// Using JSON.stringify for better readability in output.
console.log(JSON.stringify(prevState.list), JSON.stringify(state.list))
console.log(prevState.list === state.list)
}
addItemMutable(2)
addItemImmutable(3)
The goal here is to call an action from my component and get the single object from the reducer, and with that object, I will load my form.
Redux is working, but its returning the single object that I want as my whole state
My component:
const dispatch = useDispatch()
const rewardFromRedux = dispatch(getProductForUpdateAction('reward', rewardIdForUpdate))
// After getting the information from redux, I will load my component state like this:
const [title, setTitle] = useState(rewardFromRedux ? rewardFromRedux.title : '')
const [about, setAbout] = useState(rewardFromRedux ? rewardFromRedux.about : '')
My action:
// receives a product type (survey, reward) and the productId
// and call action to get the product from the reducer
export function getProductForUpdate(productType, productId) {
switch (productType) {
case 'reward':
return {
type: actions.GET_REWARD_FOR_UPDATE,
payload: productId,
}
default:
return null
}
}
My reducer:
case GET_REWARD_FOR_UPDATE:
return produce(state, draft => {
const rewardIndex = draft.campaign_products.reward.rewards.findIndex(
p => p.uuid === action.payload,
)
if (rewardIndex >= 0) {
// the problem is here, is returning the item that I want but as my whole state
// state.campaign_products.reward.rewards is my array of reward objects
return state.campaign_products.reward.rewards[rewardIndex]
}
})
How can I return only the object that I want?
Create then use a selector.
Selector
export const getRewardByIndex = (rewardIndex) => (state) => {
return state.campaign_products.reward.rewards[rewardIndex];
};
In your component
import { useSelector } from 'react-redux';
const someIndex = 0;
const reward = useSelector(getRewardByIndex(someIndex));
If I fetch this array of restos with redux:
[{
res_id: Int,
res_name: String,
res_category: String,
res_category_id: Int,
city_id: Int
}]
My action looks something like this:
export const getrestos = () => {
const resData = await response.json();
dispatch({
type: GET_RESTOS,
payload: resData
});
};
};
export const setFilters = filterSettings => {
console.log(filterSettings);
return { type: SET_FILTERS, filters: filterSettings };
};
And this is my reducer:
import { GET_RESTOS, SET_FILTERS } from '../actions/restos';
const initialState = {
restoList: [],
filteredRestos: []
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_RESTOS:
return {
restoList: action.payload
}
case SET_FILTERS:
const appliedFilters = action.filters;
const updatedFilteredRestos = state.restoList.filter(resto => {
if (appliedFilters.cityID || resto.city_id) {
resto => resto.city_id.indexOf(cityID) >= 0
return { ...state, filteredRestos: updatedFilteredRestos };
}
});
return { ...state, filteredRestos: updatedFilteredRestos };
default:
return state;
}
};
I have touchable categorys in a page, and when i touch one i want to fetch the corresponding restos for that category and show them in a flatlist. Apart from that i want to have a search bar that when I type I want to show restos by res_name and/or by res_category.
Ive tried to create selectors, but I dont understand how, i dont need an specific approach, but the most clean or efficient as possible.
Thanks in advance if anyone can give me a hint or solution!
EDIT
The problem is im getting undefined in updatedFilteredRestos.
Your reducers should be clean, dumb and all they do should be returning objects. This makes your components more testable and errors easier to catch. In my opinion, this is a perfect use-case for reselect. Here's a medium article: https://medium.com/#parkerdan/react-reselect-and-redux-b34017f8194c But the true beauty of reselect is that it will memoize for you, i.e. if your states don't change, it uses a cached version of the data.
Anyway, you should clean up your restoReducer to something to this effect.
import { GET_RESTOS, SET_FILTERS } = "../actions/restos";
const initialState = {
restoList: [],
filteredRestos: []
};
const restoReducer = (state = initialState, action) => {
switch(action.type) {
case GET_RESTOS:
return { ...state, restoList: action.payload };
case SET_FILTERS:
return { ...state, filteredRestos: action.payload };
default:
return state;
}
}
Then write your filtered resto selector:
// ../selectors/restos
import { createSelector } from "reselect";
// First, get your redux states
const getRestos = (state) => state.restos.restoList;
const getFilteredRestos = (state) => state.restos.filteredRestos;
// Next, create selectors
export const getFilteredRestoList = createSelector(
[getRestos, getFilteredRestos],
(restoList, filteredRestos) => {
// need to check for non-empty filters
// if it is, simply return the unfiltered `restoList`
if(!Array.isArray(filteredRestos) || !filteredRestos.length)
return restoList || [];
// If you do have valid filters, return filtered logic
return restoList.filter(r => filteredRestos.some(f => f.cityID === r.city_id));
);
Then, use this selector in your components:
// ../components/my-app
import { getFilteredRestoList } from "../selectors/restos";
// hook it up to your `mapStateToProps` as you would a normal state
// except this time, it's a special selector
const mapStateToProps = (state, ownProps) => {
restoList: state.restos.restoList,
filteredRestos: state.restos.filteredRestos,
filteredRestoList: getFilteredRestoList(state) //<-- this is your selector
}
Then inside your component, just reference it: this.props.filteredRestoList.
I am trying to develop an application, that is showing photos from Unsplash given a keyword. I managed to fetch specific photos using unsplash.js:
actions:
export function fetchPhotos(term) {
const unsplash = new Unsplash({
applicationId:
"id",
secret: "secret",
callbackUrl: "callback"
});
const response = unsplash.search
.photos(term, 1, 20)
.then(toJson)
.then(json => json);
return {
type: FETCH_PHOTOS,
payload: response
};
}
export function setCategory(term) {
return {
type: SET_CATEGORY,
categories: [term]
};
}
export function sortPhotos(attribute) {
return {
type: SORT_PHOTOS,
attribute
}
}
Component that renders the photos:
import React, { Component } from "react";
import { connect } from "react-redux";
import SinglePhoto from "../components/SinglePhoto";
class PhotoList extends Component {
renderPhotos() {
const { photos } = this.props;
console.log(photos);
if (!photos) {
return <p>Loading...</p>;
}
return photos.map(photo => {
const url = photo.urls.full;
const id = photo.id;
const alt = photo.description;
return <SinglePhoto url={url} key={id} alt={alt} />;
});
}
render() {
return <div>{this.renderPhotos()}</div>;
}
}
function mapStateToProps(state) {
return {
photos: state.photos,
categories: state.categories
};
}
export default connect(mapStateToProps)(PhotoList);
And reducers:
import { FETCH_PHOTOS, SORT_PHOTOS } from "../actions/types";
export default function(state = [], action) {
switch (action.type) {
case FETCH_PHOTOS:
return [...action.payload.results];
case SORT_PHOTOS:
break;
default:
return state;
}
}
What I am struggling to do is to actually sort the array of data I receive from the API according to a specific term. The response is an array of objects that makes it impossible to call it in an external component I've called Buttons that I have wanted to set the logic in:
class Buttons extends Component {
render() {
const { created_at: date } = this.props.photos;
console.log(this.props);
return (
<div className="buttons">
{/* <button onClick={() => this.props.sortPhotos(date)}>Sort by creation date</button> */}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
photos: state.photos
}
}
const mapDispatchToProps = (dispatch) => bindActionCreators({sortPhotos}, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(Buttons);
As I would need to loop over the photos to actually receive their created_at props.
I would like to sort them, for example, taking created_at into account. This would be handled by a button click (there would be other buttons for let's say likes amount and so on). I tried to do this in mapStateToProps until the moment I realized it would be impossible to call this with onClick handler.
As I have read this post, I thought it would be a great idea, however, I am not sure, how can I handle this request by an action creator.
Is there any way that I could call sorting function with an onclick handler?
One approach you can take is using a library such as Redux's reduxjs/reselect to compute derived data based on state, in this case sorted items based on some object key and/or direction. Selectors are composable and are usually efficient as they are not recomputed unless one of its arguments changes. This approach is adding properties to the reducer's state for sort key and sort order. As these are updated in the store via actions/reducers, the selector uses state to derive the elements in the resulting sorted order. You can utilize the sorted items in any connected component.
I've tried my best to recreate a complete example including actions, reducers, selectors, and store structure.
Actions - Created actions for setting sort key/direction. My example is using redux-thunk for handling async actions, but that is in no way necessary:
export const SET_SORT = 'SET_SORT';
const setSort = (sortDirection, sortKey) => ({
type: SET_SORT,
sortDirection,
sortKey
});
export const sort = (sortDirection = 'desc', sortKey = 'created_at') => dispatch => {
dispatch(setSort(sortDirection, sortKey));
return Promise.resolve();
};
Reducer - Updated initial state to keep track of a sort key and/or sort direction with photo objects being stored in a child property such as items:
const initialState = {
isFetching: false,
sortDirection: null,
sortKey: null,
items: []
};
const photos = (state = initialState, action) => {
switch (action.type) {
case FETCH_PHOTOS:
return {
...state,
isFetching: true
};
case RECEIVE_PHOTOS:
return {
...state,
isFetching: false,
items: action.photos
};
case SET_SORT:
return {
...state,
sortKey: action.sortKey,
sortDirection: action.sortDirection
};
default:
return state;
}
};
Selector - Using reselect, create selectors that retrieves items/photos, sortOrder, and sortDirection. The sorting logic can obviously be enhanced to handle other keys/conditions/etc:
import { createSelector } from 'reselect';
const getPhotosSelector = state => state.photos.items;
const getSortKeySelector = state => state.photos.sortKey;
const getSortDirectionSelector = state => state.photos.sortDirection;
export const getSortedPhotosSelector = createSelector(
getPhotosSelector,
getSortKeySelector,
getSortDirectionSelector,
(photos, sortKey, sortDirection) => {
if (sortKey === 'created_at' && sortDirection === 'asc') {
return photos.slice().sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
} else if (sortKey === 'created_at' && sortDirection === 'desc') {
return photos.slice().sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
} else {
return photos;
}
}
);
Component - Utilize selector to render items. Trigger dispatch of sort action via button click passing in a sort key and/or sort order. The linked example uses dropdowns in combination with the button click to set sort key/order:
import { getSortedPhotosSelector } from './selectors';
// ...
handleClick() {
this.props.dispatch(sort('desc', 'created_at'));
}
render() {
const { sortDirection, sortKey, items } = this.props;
<ul>
{items.map(item => <li key={item.id}>{item.created_at}</li>)}
</ul>
<button type="button" onClick={this.handleClick}>SORT</button>
}
const mapStateToProps = state => ({
items: getSortedPhotosSelector(state),
sortKey: state.photos.sortKey,
sortDirection: state.photos.sortDirection
});
export default connect(mapStateToProps)(PhotoList);
Here is a StackBlitz, demonstrating the functionality in action. It includes controlled components such as and to trigger dispatch of a sort action.
Hopefully that helps!
hard for me to word this question. but how do I access a nested key inside redux? I'd like to apply the spread operator to the key "appointment" in 'FETCH_APPOINTMENT, but i don't know how to access it. Thanks
what i tried
case 'FETCH_APPOINTMENT':
return { ...state, appointment: action.payload.data };
case 'SOME_CASE':
return {
[state && state.appointment]: state && state.appointment,
..._.mapKeys(action.payload.data, 'id'),
};
result
{undefined: undefined}
the results are them not being able to define state.appointment
This should work. You can remove the state && state.appointment truthy check by adding a default value for appointment in your reducers initialState:
const initialState = {
appointment: 'A'
};
const reducers = (state = initialState, action) => {
switch(action) {
case "SOME_CASE": {
return {
[state.appointment]: {
...[state.appointment],
_.mapKeys(action.payload.data, 'id')
}
}
}
}
}