React with Redux, big performance loss when fetching images and setting them as part of state slice - javascript

I have a React app, the state is managed with Redux.
The user can search for a game and a multitude of results, whose titles loosely match the query, will appear on submitting. Every time the user enters another query, the previous results are replaced by the new ones.
After 5-6 searches, the app slows down considerably. After the 7th search, it stops working entirely, Chrome throwing a 'page not responding' notice.
The redux slice looks like this:
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import rawg from '../../apis/rawg';
const initialState = {
results: [],
};
export const fetchGames = createAsyncThunk(
'gamesSearch/fetchGames',
async (query) => {
const response = await rawg.get('/games', {
params: {
search: query,
},
});
return response.data.results;
}
);
const gamesSearchSlice = createSlice({
name: 'gamesSearch',
initialState,
reducers: {},
extraReducers: {
[fetchGames.fulfilled]: (state, action) => {
const results = action.payload;
const parsedResults = results.map((result) => {
return {
name: result.name,
slug: result.slug,
backgroundImage: result.background_image,
genres: result.genres.map((genre) => genre.name).join(', '),
id: result.id,
released: result.released
? `${result.released.split('-')[2]}.${
result.released.split('-')[1]
}.${result.released.split('-')[0]}`
: null,
};
});
},
},
});
export default gamesSearchSlice.reducer;
export const selectResults = (state) => state.gamesSearch.results;
And the component from which the fetch is dispatched looks like so:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { fetchGames } from './gamesSearchSlice';
const SearchBar = () => {
const [query, setQuery] = useState('');
const dispatch = useDispatch();
const onSubmit = (e) => {
e.preventDefault();
if (!query) return;
dispatch(fetchGames(query));
};
return (
<div className="searchbar">
<form onSubmit={onSubmit}>
<input
className="searchbar__input"
type="text"
placeholder="Search for a game..."
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</form>
</div>
);
};
export default SearchBar;
Am I missing some detail about how React and Redux work together, or is it something wrong with my code from a fundamentals perspective (meaning: I am not handling data efficiently enough with JavaScript)?

Related

How to create global filter search with redux toolkit, react and javascripr?

I need to develop a global search filter to search the products based on the product title.
To solve the problem I'm using redux to manage the global formal state. I created a reducer to filter the titles, however, I don't know how to get the array of API objects and play in the initialState data: []; To call the API I'm using createAPI from the redux toolkit.
Some part of code:
import { createSlice } from "#reduxjs/toolkit";
const searchSlice = createSlice({
name: "search",
initialState: {
data: [], // array of objects from API
filteredTitle: [],
isLoading: true
},
reducers: {
getData: (state, action) => {
state.data = action.payload;
},
searchByName: (state, action) => {
const filteredResult = state.data.filter((product) =>
product.title.toLowerCase().includes(action.payload.toLowerCase())
);
return {
...state,
filteredTitle:
action.payload.length > 0 ? filteredResult : [...state.data]
};
}
}
});
export const { searchByName, getData } = searchSlice.actions;
export default searchSlice.reducer;
In App.js
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { searchByName, getData } from "./features/slice";
import { useGetAllProductsQuery } from "./services/api";
export default function App() {
const dispatch = useDispatch();
const filteredTitle = useSelector((state) => state.search.filteredTitle);
const [searchTerm, setSearch] = useState("");
const { data } = useGetAllProductsQuery();
console.log("data", data);
const changeSearchTerm = (e) => {
setSearch(e.target.value);
};
useEffect(() => {
dispatch(searchByName(searchTerm));
dispatch(getData(data));
}, [searchTerm, dispatch]);
return (
<div>
<input onChange={changeSearchTerm} type="text" value={searchTerm} />
<div>
{filteredTitle.map((user) => (
<div>{user.name}</div>
))}
</div>
</div>
);
}
CodeSandbox
Redux Toolkit Query is made, so you won't interact with Redux store with thunk functions directly (that's what they say in their documentation).
You can only use transformResponse once creating the query. I haven't come across any ways, to be able to add an extra reducer to the slices that are made with RTK Query.
The first way, is to store the data that you are getting from the query, inside another slice, and then use a reducer to filter your data.
The second way is to create a slice, and using createAsyncThunk, fetch your data, then treat your data as you want it. I can leave an example for you:
export const getName = createAsyncThunk('getNameFromBE',
async name => {
await fetch('https://example.com/studentID)
}
)
export const studnetSlice = createSlice({
name: 'studentSlice',
initialState,
reducers: {
aReducer : (state, action) => {state.name = action.payload}
},
extraReducers: {
[getName .pending]: state => {
state.loading = true
state.failed = false
},
[getName .fulfilled]: (state, action) => {
state.name= action.payload
state.loading = false
},
[getName .rejected]: state => {
state.loading = false
state.rejected = true
},
}
})
Here you have a slice, with thunk and normal reducers, with a state that is accessible, but the downside is that you have to manage fetch completely manually (can be an advantage too), and also the call state (pending, fulfilled, rejected) must be done manually, which takes a lot of boilerplate code writing.

How to display newly made (nested?) Object using React-Redux without refreshing the page?

While I have a little experience with using React-Redux, I'm unsure of what to do here to fix this problem...
Basically, I have Users, who have many Binders, Binders have many Decks, and Decks that have many (not implemented) Flashcards.
This issue is happening on an page where I'm trying to display the Decks of an specific binder, and an User can create or update Decks, in my (Binder.jsx).
My professor has told me the problem is with my bindersSlice.js.
Basically, I'm currently updating my list of Decks in my decksSlice, but it's my bindersSlice that is displaying the list. So, because I'm not doing anything to update the Decks in bindersSlice, it only updates after the page is refreshed.
How should I go about fixing this issue? Any good solutions? Do I need to move my Action Creators from decksSlice into bindersSlice???
EDIT: Okay, I've gone over everything with my professor again, and I think I realize my problem. Basically, I'm needing my bindersSlice to realize that it has an new Deck. So, after I do dispatch(newDeck()), I'm going to need to dispatch an (new) Action Creator/Reducer to add the new Deck object, right after after used the dispatch(newDeck()). Right now, I am unsure of the best method to do this, so any ideas would be most welcome...
My code for an MRE (Minimal Reproducible Example):
class Binder < ApplicationRecord
has_many :decks
has_many :flashcards, through: :decks
end
class Deck < ApplicationRecord
belongs_to :binder
end
bindersSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { headers } from "../../Globals";
export const fetchBinders = createAsyncThunk("binders/fetchBinders", async () => {
// return GET request
});
export const newBinder = createAsyncThunk("binders/newBinder", async (binder) => {
return fetch("/binders", {
method: "POST",
headers: headers,
body:JSON.stringify({binder})
}).then((r) => r.json())
});
const bindersSlice = createSlice({
name: "binders",
initialState: {
entities: [], // Array of Binders, each with an Array of Decks
errorMessages: null,
},
extraReducers(builder){
builder
.addCase(fetchBinders.fulfilled, (state, action) => {
state.entities = action.payload;
})
.addCase(newBinder.fulfilled, (state, action) => {
if(action.payload.errors) state.errorMessages = action.payload.errors;
else{
state.errorMessages = null;
state.entities.push(action.payload);
}
})
}
});
export default bindersSlice.reducer;
decksSlice.js
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import { headers } from "../../Globals";
export const fetchDecks = createAsyncThunk("decks/fetchDecks", async () => {
// return GET request
});
export const newDeck = createAsyncThunk("decks/newDeck", async (payload) => {
return fetch(`/decks`, {
method: "POST",
headers: headers,
body: JSON.stringify(payload)
}).then((r) => r.json())
});
const decksSlice = createSlice({
name: "decks",
initialState: {
entities: [], // Array of Decks
errorMessages: null,
},
extraReducers(builder){
builder
.addCase(fetchDecks.fulfilled, (state, action) => {
state.entities = action.payload;
})
.addCase(newDeck.fulfilled, (state, action) => {
if(action.payload.errors) state.errorMessages = action.payload.errors;
else{
state.errorMessages = null;
state.entities.push(action.payload);
}
})
}
});
export default decksSlice.reducer;
DeckInput.jsx
import React, {useState} from 'react';
import { useParams } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { newDeck } from './decksSlice';
function DeckInput(){
const dispatch = useDispatch();
const params = useParams();
const binders = useSelector((state) => state.binders.entities);
const errors = useSelector((state) => state.decks.errorMessages);
const [name, setName] = useState("");
const thisBinder = binders.find(binder => {
return binder.id.toString() === params.id
});
function handleSubmit(e){
e.preventDefault();
dispatch(newDeck({ // Coming from decksSlice
binder_id: thisBinder.id,
name: name
}));
}
return (
{/* Omit form and form logic */}
)
}
export default DeckInput;

Redux Toolkit State issue when sending to child component

I am creating react redux application using redux toolkit and I'm passing some props to child component, it supposed to be one post because I'm using a map in parent component and passing one data to each component.
I'm trying to do Edit button and when clicking the "Edit button" trying to send ID to redux store but there is an error. If anyone know the answer please let me know.
Below is my redux slice:
import { createAsyncThunk, createSlice } from "#reduxjs/toolkit";
import axios from "axios";
const initialState = {
allPosts: [],
loading: "idle",
error: "",
currentId: "",
};
export const fetchAlltAsync = createAsyncThunk(
"allposts",
async (_, thunkAPI) => {
try {
const response = await axios.get("http://localhost:5000/posts/");
// The value we return becomes the `fulfilled` action payload
return response.data;
} catch (error) {
throw thunkAPI.rejectWithValue({ error: error.message });
}
}
);
export const postsingleAsync = createAsyncThunk(
"postsingleAsync",
async (post, { dispatch }) => {
const response = await axios.post("http://localhost:5000/posts/", post);
return response.data;
}
);
export const idsingleAsync = createAsyncThunk(
"idsingleAsync",
async (id, updatedpost) => {
const response = await axios.patch(
`http://localhost:5000/posts/${id}`,
updatedpost
);
return response.data;
}
);
export const postSlice = createSlice({
name: "posts",
initialState,
// The `reducers` field lets us define reducers and generate associated actions
reducers: {
// Use the PayloadAction type to declare the contents of `action.payload`
newsetcurrentId: (state, action) => {
state.currentId = action.payload;
},
},
// The `extraReducers` field lets the slice handle actions defined elsewhere,
// including actions generated by createAsyncThunk or in other slices.
extraReducers: (builder) => {
builder.addCase(fetchAlltAsync.pending, (state) => {
state.allPosts = [];
state.loading = "Loading";
});
builder.addCase(fetchAlltAsync.fulfilled, (state, action) => {
state.allPosts = action.payload;
state.error += "Loaded";
});
builder.addCase(fetchAlltAsync.rejected, (state, action) => {
state.allposts = "data not loaded";
state.loading = "error";
state.error = action.error.message;
});
builder.addCase(idsingleAsync.fulfilled, (state, action) => {
state.currentId = action.payload;
});
},
});
export const { setcurrentId, newsetcurrentId } = postSlice.actions;
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state) => state.counter.value;
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on current state.
export const incrementIfOdd = (amount) => (dispatch, getState) => {};
export default postSlice.reducer;
Below is my parent component:
import React, { useEffect, useState } from "react";
import Post from "./Post";
import { useSelector, useDispatch } from "react-redux";
const Posts = ({ SETCURRENTID, CURENTID }) => {
// const dispatch = useDispatch();
const posts = useSelector((state) => state.posts.allPosts);
return (
<div>
{posts &&
posts.map(({ _id, ...rest }) => (
<Post key={_id} rest={rest} id={_id} />
))}
</div>
);
};
export default Posts;
This is my child component:
import React from "react";
import moment from "moment";
import { idsingleAsync, newsetcurrentId } from "../../features/postSlice";
import { useSelector, useDispatch } from "react-redux";
const Post = ({ rest, _id }) => {
const dispatch = useDispatch();
console.log(rest, "gff");
//const { id } = this.rest._id;
const handleClick = () => dispatch(newsetcurrentId());
return (
<div>
<h1>{rest.title}</h1>
<img
style={{ maxWidth: "250px", border: "12px solid purple" }}
alt="d"
src={rest.selectedFile}
/>
<h2>{moment(rest.createdAt).fromNow()}</h2>
<button onClick={() => dispatch(newsetcurrentId(rest._id))}> edit</button>
<h5>{rest.tags.map((tag) => `#${tag} `)}</h5>
<h5 onClick={() => {}}>{rest.likeCount}</h5>
<button onClick={() => {}}>Delete</button>
</div>
);
};
export default Post;
This is the redux error:
requestId(pin):undefined
TL;DR
Instead of rest._id , try passing the id prop to your newsetcurrentId dispatch:
const Post = ({ rest, id }) => { //Change _id to id
const dispatch = useDispatch();
const handleClick = () => dispatch(newsetcurrentId());
return (
<div>
<h1>{rest.title}</h1>
<img
style={{ maxWidth: "250px", border: "12px solid purple" }}
alt="d"
src={rest.selectedFile}
/>
<h2>{moment(rest.createdAt).fromNow()}</h2>
{/* pass id here */}
<button onClick={() => dispatch(newsetcurrentId(id))}> edit</button>
<h5>{rest.tags.map((tag) => `#${tag} `)}</h5>
<h5 onClick={() => {}}>{rest.likeCount}</h5>
<button onClick={() => {}}>Delete</button>
</div>
);
};
Explanation
When you are doing this destructuring:
posts.map(({ _id, ...rest }) => ( your rest object will actually contain all the post properties apart from _id so you don't actually have rest._id which you are trying to access on your Post child.
Additionally, you are passing id={_id} as a prop from the parent to the child, so you don't actually have an _id prop on your Post component (change it to id).

Redux state not updating after action dispatched

I have a form for users to enter their details and press submit. This is supposed to dispatch an action and update the state by .concat() a class to it. Unfortunately the state isn't updating and I don't know why. If I take out useCallBack() or useEffect() from the code , the emulator freezes and I suspect infinite loops.
Redux Reducer
// Initialised class
import newAccount from '../../models/newAccount'
import { CREATE_ACCOUNT } from '../actions/meals'
const initialState = {
account: [],
}
const addPerson = (state=initialState, action) =>{
switch(action.type){
case CREATE_ACCOUNT:
const newAccount = new newAccount(
Date.now().toString(),
action.accountData.name,
action.accountData.image,
action.accountData.email,
action.accountData.password
)
return { ...state, account: state.account.concat(newAccount) }
default:
return state
}
}
export default addPerson
Redux action
export const CREATE_ACCOUNT = 'CREATE_ACCOUNT'
export const newAccount = (Id,name,image, email, password) => {
return {type: CREATE_ACCOUNT, accountData:{
Id: Date.now().toString(),
name: name,
image: image,
email: email,
password: password
}
}
}
The class
class newAccount {
constructor(
id,
name,
image,
email,
password
){
this.id = id;
this.name = name;
this.image = image;
this.email = email;
this.password = password;
}
}
export default newAccount
The Component
import React, { useState, useCallback, useEffect } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {newAccount} from '../Store/actions/accounts'
import ImagePicker from '../Components/ImagePicker'
const AddScreen = (props) => {
const dispatch = useDispatch()
const [name, setName] = useState('')
const [selectedImage, setSelectedImage] = useState('')
const email = useSelector(state => state.account.email)
const password = useSelector(state => state.account.password)
const handleSubmit = useCallback(() => {
dispatch(newAccount(Date.now(),name,selectedImage,email,password))
},[dispatch, name, selectedImage, email, password])
useEffect(() => { handleSubmit
props.navigation.setParams({handleSubmit: handleSubmit})
},[handleSubmit])
return (
<View style={styles.container}>
<View style={styles.card}>
<ImagePicker onImageSelected={selectedImage} />
<AddForm email={email} password={password}/>
<TextInput
onChangeText={name => setName(name)}
value={name}
/>
</View>
</View>
)
}
export default AddScreen
AddScreen.navigationOptions = (navigationData) => {
const submit = navigationData.navigation.getParam('handleSubmit')
return {
headerTitle: 'Create Account',
headerRight: () => (
<TouchableOpacity onPress={submit}>
<Text style={styles.createOrange}>Create</Text>
</TouchableOpacity>
)
}
}
I really don't know why it's not updating .
first of all, you shouldn't store classes in the redux store, the store should only exists of plain objects. but if you really want to store the class:
The real problem seams to be return { ...state, account: state.account.concat(newAccount) }. here you concat the existing array with the new class, but that doesn't work.
your store looks like this if you do so:
{
account: [{
email: "..."
id: "..."
image: "..."
name: "..."
password: "...
}],
}
so your selector (state.account.email) will return undefined. you can use (state.account[0].email)
or you can fix it by fixing the real problem:
return { ...state, account: newAccount }
also your initialState shouldn't be a an array for account as it will never be an array, it will be an Account class (this is why you don't get an error by what you are doing). set it to null.
const initialState = {
account: null,
}
I really don't know why this doesn't work. Just want to give you an advice to make it more simple and clearer (from my point of view):
You can drop side effects like useEffect. To achieve this just move local state to redux state and then you will be able to just dispatch the action from your navigationOptions component. It could look like:
const AddScreen = () => {
const name = useSelector(...);
...
const password = useSelector(...);
// trigger action on something changes, for instance like that:
const onChange = (key, value) = dispatch(newAccountChange({[key]: value}))
// return tree of components
}
export const submitNewAccount = () => {
return (dispatch, getState) => {
const { id, name, ... } = getState().account;
dispatch(newAccount(id, name, ...));
};
}
AddScreen.navigationOptions = (navigationData) => {
const dispatch = useDispatch();
const submit = dispatch(submitNewAccount());
...
}
I used redux-thunk in this example.
I believe, this approach will give you more flexible way to debug and extend your business logic.

Pass data between two independent components in ReactJS

I'm building a web-application with ReactJS and that needs me to implement a Search. So far, the search has been implemented (I'm using Fuse.js library for this) using a form with an input element. The form is implemented in the NavBar and after the user types a search-query, he is redirected to 'localhost:3000/search' URL where he can see the results corresponding to his query.
Here is the code I'm using for the form in the SearchBar.
import React, { useState } from 'react';
import { Form, FormControl } from 'react-bootstrap';
import { ReactComponent as SearchLogo } from '../../lib/search-logo.svg';
const SearchBar = () => {
const [searchQuery, setSearchQuery] = useState({ query: '' });
const searchQueryHandler = (event) => {
event.preventDefault();
setSearchQuery({ query: event.target.value });
};
const onFormSubmit = (event) => {
event.preventDefault();
window.location.href = "/search";
}
return (
<Form inline className="nav-search-form" onSubmit={onFormSubmit}>
<SearchLogo className="search-logo" />
<FormControl
type="text"
placeholder="Search spaces of interest"
className="nav-search"
value={searchQuery.query}
onChange={searchQueryHandler} />
</Form>
);
}
export default SearchBar;
I need to display the corresponding results in another SearchPage which will take the query from this component after submission and then display the results. Here is the code I have written for it.
import React, { useState, useRef } from 'react';
import { Col, Container, Row } from 'react-bootstrap';
import SpaceCardGrid from '../space-card-grid/space-card-grid';
import useSpaces from '../../utils/firebase/hooks/useSpaces';
import moment, { Moment } from 'moment';
import { roundTime } from '../../utils/date';
import Fuse from 'fuse.js';
const SearchPage = (queries) => {
const [date, setDate] = useState<[Moment, Moment]>([moment(new Date()), moment(new Date())]);
const [time, setTime] = useState([roundTime(), roundTime(30)]);
const [dateRangeType, setDateRangeType] = useState<'week' | 'day' | 'now'>('day');
const spaceCardGridRef = useRef(null);
const spaces = useSpaces(dateRangeType, date, time, 0);
const options = {
shouldSort: true,
keys: ['title', 'description'],
};
const fuse = new Fuse(spaces, options);
let filteredspaces = spaces;
if (queries.query !== '') {
const result = fuse.search(queries.query);
console.log(result);
filteredspaces = [];
result.forEach((space) => {
filteredspaces.push(space.item);
});
}
return (
<div>
<Container fluid className="bottom-container">
<Row style={{ justifyContent: 'center', alignItems: 'flex-start' }}>
<Col>
<div className="grid-root">
<SpaceCardGrid spaces={filteredspaces} spaceCardGridRef={spaceCardGridRef} />
</div>
</Col>
</Row>
</Container>
</div>
);
};
export default SearchPage;
Just for additional information useSpaces() is a function that gives me all the data (and it does so correctly), and filteredspaces is the final results array that I wish to display on the screen. All these things are perfectly working.
I'm stuck on how to pass the query between the two components though. The queries I have used in SearchPage(queries) is a dummy variable. I'm new to React, and I have learned about Redux, but it seems a lot of work (I might be wrong) for simply passing a value between 2 components. As you can clearly observe, the components aren't related but are independent. Is there a simple way to do this? Any help will be greatly appreciated!
you could use useContenxt along with useReducer hooks for a simpler state structure. I created a small example here. you can find more reference at docs
basically at root from your appplication you would start by creating a context and pass dispatch and query as values to your Provider:
export const QueryDispatch = React.createContext("");
const initialState = { query: "" };
export default function App() {
const [{ query }, dispatch] = useReducer(queryReducer, initialState);
return (
<QueryDispatch.Provider value={{ dispatch, query }}>
<SearchBar />
<SearchPage />
</QueryDispatch.Provider>
);
}
where queryReducer could be like:
export default function (state, action) {
switch (action.type) {
case 'update':
return {query: action.query};
default:
return state;
}
}
and at any component you could consume from your provider:
at your searchBar
import React, { useContext } from "react";
import { QueryDispatch } from "./App";
const SearchBar = () => {
const const { dispatch, query } = useContext(QueryDispatch);
const searchQueryHandler = (event) => {
dispatch({ type: "update", query: e.target.value })
};
..code
<FormControl
type="text"
placeholder="Search spaces of interest"
className="nav-search"
value={query}
onChange={searchQueryHandler} />
and at your SearchPage
import React, { useContext } from "react";
import { QueryDispatch } from "./App";
const SearchPage = () => {
const { query } = useContext(QueryDispatch);

Categories

Resources