I am trying to call API with Redux, and it calls again based on a form submission.
which means if query is none, it returns all lists or it returns lists that match the query.
// List.tsx
import React, { useEffect, useState } from "react";
import { connect } from "react-redux";
import { getFeatures, increasePage } from "../../redux/featuresSlice";
import { FeatureItem } from "../../components/featureItem";
interface IFeatureProp {
featureObject: any;
getFeaturesWith: any;
}
const FeatureList = ({
featureObject,
getFeaturesWith,
}: // page,
IFeatureProp) => {
const [keywords, setKeywords] = useState("");
const fetchData = async () => {
getFeaturesWith(featureObject.page, keywords);
};
const handleSubmit = (e: any) => {
e.preventDefault();
fetchData();
};
useEffect(() => {
console.log(keywords);
fetchData();
}, []);
return (
<div className="max-w-screen-xl mx-auto mt-8">
<div className="px-12">
<div className="relative">
<div className="relative">
<form onSubmit={handleSubmit}>
<div className="absolute top-0 bottom-0 left-0 flex items-center px-5">
</div>
<input
type="text"
placeholder="Search..."
value={keywords}
onChange={e => setKeywords(e.target.value)}
/>
</form>
</div>
<ul>
{featureObject.features
.map((c: any) => {
return (
<FeatureItem
id={c.id}
name={c.name}
desc={c.desc}
/>
);
})}
</ul>
</div>
</div>
</div>
);
};
function mapStateToProps(state: any) {
return { featureObject: state.featuresReducer.explore };
}
function mapDispatchToProps(dispatch: any) {
return {
getFeaturesWith: (page: any, keyword: string) =>
dispatch(getFeatures(page, keyword)),
increasePageWith: () => dispatch(increasePage(1)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(FeatureList);
//featuresSlice.js
import { createSlice } from "#reduxjs/toolkit";
import api from "../api";
const featuresSlice = createSlice({
name: "features",
initialState: {
explore: {
page: 1,
features: [],
},
favs: [],
},
reducers: {
setExploreFeatures(state, action) {
const { explore } = state;
const { payload } = action;
payload.features.forEach(payloadFeature => {
const exists = explore.features.find(
savedFeature => savedFeature.id === payloadFeature.id
);
if (!exists) {
explore.features.push(payloadFeature);
}
});
state.explore.page = payload.page;
},
},
});
export const { setExploreFeatures, increasePage, setFavs, setFav } =
featuresSlice.actions;
export const getFeatures = (page, keyword) => async (dispatch, getState) => {
const {
usersReducer: { token },
} = getState();
try {
const {
data: { results },
} = await api.features(token, page, keyword);
dispatch(
setExploreFeatures({
features: results,
page: 1,
})
);
} catch (e) {
console.warn(e);
}
};
export default featuresSlice.reducer;
when I submit keyword, it works as I expected at backend.
[07/Feb/2022 01:55:23] "GET /api/v1/features/?search=abcd HTTP/1.1" 200 3615
And I see only lists that match the query in redux-debugger.
But it doesn't re-render the page, which means I only see whole lists.
Is there what I can do for updating state?
The issue is that your reducer is mutating the state, which should never be the case within the Redux ideology.
Instead, you should be creating a new state object (with your changes applied) and return it from the reducer. And if you don't want to modify the state - just return the original one kept intact. All your reducers should be like that.
reducers: {
setExploreFeatures(state, action) {
const { payload } = action;
const newFeatures = payload.features.filter(x => !state.explore.features.some(y => y.id === x.id));
return { ...state, explore: { ...state.explore, features: [...state.explore.features, ...newFeatures], page: payload.page } };
},
},
along these lines ^^
Related
I am dispatching an action that is supposed to take an input from the user and store it in a database. However, when I inspect my posts state in redux after the action is dispatched, there is a null value appended to the state array before the actual post. This is preventing me from working with the actual data in the posts array. Basically I'm wondering how to prevent null from being appended each time I dispatch a new post. Here are the relevant code snippets and images.
Post Reducer:
import { enableAllPlugins, produce } from 'immer';
enableAllPlugins();
const initialState = {
posts: [],
loading: false,
error: false,
uploading: false,
};
const postReducer = produce((draftstate, action = {}) => {
switch (action.type) {
case 'UPLOAD_START':
draftstate.loading = true;
draftstate.error = false;
case 'UPLOAD_SUCCESS':
draftstate.posts.push(action.data);
draftstate.uploading = false;
draftstate.error = false;
case 'UPLOAD_FAIL':
draftstate.uploading = false;
draftstate.error = true;
default:
return draftstate;
}
}, initialState);
export default postReducer;
Upload Post action:
export const uploadPost = (data) => async (dispatch) => {
dispatch({ type: 'UPLOAD_START' });
try {
const newPost = await UploadApi.uploadPost(data);
console.log('new post before', newPost);
dispatch({ type: 'UPLOAD_SUCCESS', data: newPost.data });
} catch (error) {
console.log(error);
dispatch({ type: 'UPLOAD_FAIL' });
}
};
Share Post code:
import React, { useState, useRef } from "react";
import ProfileImage from "../../img/profileImg.jpg";
import "./PostShare.css";
import { UilScenery } from "#iconscout/react-unicons";
import { UilPlayCircle } from "#iconscout/react-unicons";
import { UilLocationPoint } from "#iconscout/react-unicons";
import { UilSchedule } from "#iconscout/react-unicons";
import { UilTimes } from "#iconscout/react-unicons";
import { useSelector, useDispatch } from "react-redux";
import { uploadImage, uploadPost } from "../../actions/uploadAction";
const PostShare = () => {
const loading = useSelector((state) => state.postReducer.uploading);
const [image, setImage] = useState(null);
const imageRef = useRef();
const desc = useRef();
const dispatch = useDispatch();
const { user } = useSelector((state) => state.authReducer.authData);
// handle Image Change
const onImageChange = (event) => {
if (event.target.files && event.target.files[0]) {
let img = event.target.files[0];
setImage(img);
}
};
const reset = () => {
setImage(null);
desc.current.value = "";
};
const handleSubmit = async (e) => {
e.preventDefault();
const newPost = {
userId: user._id,
desc: desc.current.value,
};
if (image) {
const data = new FormData();
const filename = Date.now() + image.name;
data.append("name", filename);
data.append("file", image);
newPost.image = filename;
console.log(newPost);
try {
dispatch(uploadImage(data));
} catch (error) {
console.log(error);
}
}
dispatch(uploadPost(newPost));
reset();
};
return (
<div>
<div className="PostShare">
<img src={ProfileImage} alt="" />
<div>
<input
ref={desc}
required
type="text"
placeholder="What's happening"
/>
<div className="postOptions">
<div
className="option"
style={{ color: "var(--photo)" }}
onClick={() => imageRef.current.click()}
>
<UilScenery />
Photo
</div>
<div className="option" style={{ color: "var(--video" }}>
<UilPlayCircle />
Video
</div>
<div className="option" style={{ color: "var(--location)" }}>
<UilLocationPoint />
Location
</div>
<div className="option" style={{ color: "var(--shedule)" }}>
<UilSchedule />
Schedule
</div>
<button
className="button ps-button"
onClick={handleSubmit}
disabled={loading}
>
{loading ? "Uploading..." : "Share"}
</button>
<div style={{ display: "none" }}>
<input
type="file"
name="myImage"
ref={imageRef}
onChange={onImageChange}
/>
</div>
</div>
{image && (
<div className="previewImage">
<UilTimes onClick={() => setImage(null)} />
<img src={URL.createObjectURL(image)} alt="" />
</div>
)}
</div>
</div>
</div>
);
};
export default PostShare;
I would be glad to provide any other details if that helps.
Update with other portions of code:
Dispatcher of RETRIEVING_SUCCESS:
import * as PostApi from '../api/PostRequest';
export const getTimelinePosts = (id) => async (dispatch) => {
dispatch({ type: 'RETRIEVING_START' });
try {
const { data } = await PostApi.getTimelinePosts(id);
dispatch({ type: 'RETRIEVING_SUCCESS', data: data });
} catch (error) {
dispatch({ type: 'RETRIEVING_FAIL' });
console.log(error);
}
};
getTimelinePosts usage:
import React, { useEffect } from 'react';
import './Posts.css';
import { PostsData } from '../../Data/PostsData';
import { useDispatch, useSelector } from 'react-redux';
import { getTimelinePosts } from '../../actions/postAction';
import Post from '../Post/Post';
const Posts = () => {
const dispatch = useDispatch();
const { user } = useSelector((state) => state.authReducer.authData);
let { posts, loading } = useSelector((state) => state.postReducer);
console.log('posts content', posts);
useEffect(() => {
dispatch(getTimelinePosts(user._id));
}, []);
return (
<div className="Posts">
{/* {posts.map((post, id) => {
return <Post data={post} id={id}></Post>;
})} */}
</div>
);
};
export default Posts;
in postReducer, let's remove the default on the switch statement, we don't need it on reducer because other actions will come here and the code make all states return the initial state.
After showing the content for searched item, while removing the letters from search bar not showing the contents correctly. How to show the contents based on the word which is there in search bar. I have started to learn redux. So need some suggestions
import logo from "./logo.svg";
import "./App.css";
import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
function App() {
const [name, setName] = useState("");
const [searchTerm, setSearchterm] = useState("");
const dispatch = useDispatch();
const data = useSelector((state) => state.add);
console.log(data, "dfata");
const handleChange = (e) => {
setName(e.target.value);
};
console.log(name);
if (data.length == 0) {
return <p>No data</p>;
}
const Submithandler = () => {
dispatch({ type: "ADD_ITEM", name });
setName("");
};
const handleSearch = (e) => {
setSearchterm(e.target.value);
};
const submitSerach = () => {
dispatch({ type: "SEARCH_ITEM", searchTerm });
};
const reset = () => {
dispatch({ type: "RESET", searchTerm });
};
return (
<div className="App">
{data.loading && <p>loading</p>}
<input value={searchTerm} onChange={(e) => handleSearch(e)} />
<button onClick={() => submitSerach()}>search</button>
<button onClick={() => reset()}>reset</button>
<input value={name} onChange={handleChange} />
<button onClick={Submithandler}>Add</button>
{data.item.length === 0 && <p>no item</p>}
{data.item.map((dta, i) => {
return (
<div>
{dta}
<button
onClick={() => dispatch({ type: "REMOVE_ITEM", name: dta })}
>
Remove
</button>
</div>
);
})}
</div>
);
}
export default App;
const INITIAL_STATE = {
item: [],
loading: false,
};
function addReducer(state = INITIAL_STATE, action) {
switch (action.type) {
case "ADD_ITEM":
console.log(action, "ahghsgda");
return { item: [...state.item, action.name] };
case "REMOVE_ITEM":
console.log(action, "REMOPVE");
return {
item: state.item.filter((inditem) => inditem !== action.name),
};
case "SEARCH_ITEM":
console.log(action, "ahghsgda");
const data = [...state.item];
return {
loading: true,
item: [data.filter((product) => product.includes(action.searchTerm))],
};
case "RESET":
return {
item: [...state.item],
};
default:
return state;
}
}
export default addReducer;
After showing the content for searched item, while removing the letters from search bar not showing the contents correctly
Can someone help me in implementing the debounce functionality using creatApi with query implementation from redux toolkit.
Thanks in advance.
I personally didn't find any debounce implementation in RTK Query out-of-the-box. But you can implement it yourself.
Define an api. I'm using an openlibrary's one:
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react';
type BooksSearchResult = {
docs: Book[];
};
type Book = {
key: string;
title: string;
author_name: string;
first_publish_year: number;
};
export const booksApi = createApi({
reducerPath: 'booksApi',
baseQuery: fetchBaseQuery({ baseUrl: 'http://openlibrary.org/' }),
endpoints: builder => ({
searchBooks: builder.query<BooksSearchResult, string>({
query: term => `search.json?q=${encodeURIComponent(term)}`,
}),
}),
});
export const { useSearchBooksQuery } = booksApi;
Next thing you need is debounce hook, which guarantees that some value changes only after specified delay:
function useDebounce(value: string, delay: number): string {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
Use debounce hook on your search form:
import React, { useEffect, useState } from "react";
import BookSearchResults from "./BookSearchResults";
function useDebounce(value: string, delay: number): string {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
const DebounceExample: React.FC = () => {
const [searchTerm, setSearchTerm] = useState("");
const debouncedSearchTerm = useDebounce(searchTerm, 500);
return (
<React.Fragment>
<h1>Debounce example</h1>
<p>Start typing some book name. Search starts at length 5</p>
<input
className="search-input"
type="text"
placeholder="Search books"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<BookSearchResults searchTerm={debouncedSearchTerm}></BookSearchResults>
</React.Fragment>
);
};
export default DebounceExample;
Use the search query hook in search results component. It uses its own state for search term value, which is very convenient if you want to add extra "filters" for debounced value (for example, start query only when search term's length is greater than some value).
import React, { useState, useEffect } from "react";
import { useSearchBooksQuery } from "./booksApi";
type BookSearchResultsProps = {
searchTerm: string;
};
const BookSearchResults: React.FC<BookSearchResultsProps> = ({
searchTerm
}: BookSearchResultsProps) => {
const [filteredSearchTerm, setFilteredSearchTerm] = useState(searchTerm);
const { data, error, isLoading, isFetching } = useSearchBooksQuery(
filteredSearchTerm
);
const books = data?.docs ?? [];
useEffect(() => {
if (searchTerm.length === 0 || searchTerm.length > 4) {
setFilteredSearchTerm(searchTerm);
}
}, [searchTerm]);
if (error) {
return <div className="text-hint">Error while fetching books</div>;
}
if (isLoading) {
return <div className="text-hint">Loading books...</div>;
}
if (isFetching) {
return <div className="text-hint">Fetching books...</div>;
}
if (books.length === 0) {
return <div className="text-hint">No books found</div>;
}
return (
<ul>
{books.map(({ key, title, author_name, first_publish_year }) => (
<li key={key}>
{author_name}: {title}, {first_publish_year}
</li>
))}
</ul>
);
};
export default BookSearchResults;
Full example is available here.
In my case the following solution worked well.
I used component way of debouncing. Use any debounce fn, in my way:
npm i debounce
And then in component
import debounce from 'debounce';
const Component = () => {
const { data, refetch } = useYourQuery();
const [mutate] = useYourMutation();
const handleDebouncedRequest = debouce(async () => {
try {
// you can use refetch or laze query way
await refetch();
await mutate();
} catch {}
}, 2000);
// ...other component logic
}
after the first render our request hook will try to send the request we can bypass this with the skipToken
the request will not be sent until searchTerm returns some value
import { useDebounce } from 'use-debounce'
import { skipToken } from '#reduxjs/toolkit/query'
const [storeName, setStoreName] = useState('')
const [searchTerm] = useDebounce(storeName, 1500)
const { data } = useSearchStoresRequestQuery(searchTerm || skipToken)
more about skipToken: https://redux-toolkit.js.org/rtk-query/usage/conditional-fetching
and also inside my useSearchStoresRequestQuery
endpoints: (builder) => ({
getStoresWithSearchRequest: builder.query({
query: ({searchTerm}) => {
return {
url: `admin/v1/stores?searchTerm?${searchTerm}`,
method: 'GET',
}
},
I useMutation to send message ,but the message list in chat window not change. I found that the cache has changed . Please help , I can't understand.
The useQuery not work . UI have no change :(
But~! When I put them in one js file. it works.... why???
The version I used is #apollo/react-hooks 3.1.1
Parent window.js
import React from 'react';
import { useQuery } from "#apollo/react-hooks";
import { GET_CHAT } from "#/apollo/graphql";
import ChatInput from "#/pages/chat/components/input";
const ChatWindow = (props) => {
const { chatId, closeChat } = props;
const { data, loading, error } = useQuery(GET_CHAT, { variables: { chatId: chatId } });
if (loading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
const { chat } = data;
return (
<div className="chatWindow" key={'chatWindow' + chatId}>
<div className="header">
<span>{chat.users[1].username}</span>
<button className="close" onClick={() => closeChat(chatId)}>X</button>
</div>
<div className="messages">
{chat.messages.map((message, j) =>
<div key={'message' + message.id} className={'message ' + (message.user.id > 1 ? 'left' : 'right')}>
{message.text}
</div>
)}
</div>
<div className="input">
<ChatInput chatId={chatId}/>
</div>
</div>
);
};
export default ChatWindow;
Child input.js
import React, { useState } from 'react';
import { useApolloClient, useMutation } from "#apollo/react-hooks";
import { ADD_MESSAGE, GET_CHAT } from "#/apollo/graphql";
const ChatInput = (props) => {
const [textInput, setTextInput] = useState('');
const client = useApolloClient();
const { chatId } = props;
const [addMessage] = useMutation(ADD_MESSAGE, {
update(cache, { data: { addMessage } }) {
const { chat } = client.readQuery({
query: GET_CHAT,
variables: {
chatId: chatId
}
});
chat.messages.push(addMessage);
client.writeQuery({
query: GET_CHAT,
variables: {
chatId: chatId
},
data: {
chat
}
});
}
});
const onChangeInput = (event) => {
event.preventDefault();
setTextInput(event.target.value);
};
const handleKeyPress = (event, chatId, addMessage) => {
if (event.key === 'Enter' && textInput.length) {
addMessage({
variables: {
message: {
text: textInput,
chatId: chatId
}
}
});
setTextInput('');
}
};
return (
<input type="text"
value={textInput}
onChange={(event) => onChangeInput(event)}
onKeyPress={(event) => handleKeyPress(event, chatId, addMessage)}
/>
);
};
export default ChatInput;
You probably solved the issue by now, but for the record:
Your code mutates the chat state:
chat.messages.push(addMessage);
State should not be mutated (see the React setState Docs for more details).
Contruct a new array instead:
const newChat = [...chat, addMessage]
I'm trying to delete an item from a collection in Firestore by referencing the id of the selected item. I'm successfully passing on the id by mapDispatchToProps until the action but stops short when trying to delete in Firestore by the delete(). I think the problem may be in the my method to delete in firestore as it stops there. Can anyone kindly please tell me what could be wrong with my code?
import React from 'react'
import { connect } from 'react-redux'
import { firestoreConnect } from "react-redux-firebase";
import { compose } from 'redux'
import { Redirect } from 'react-router-dom'
import moment from 'moment'
import { deleteProject } from '../../store/actions/projectActions'
const handleClick = (e, prop) => {
e.preventDefault()
deleteProject(prop)
console.log(prop)
}
const ProjectDetails = (props) => {
const { auth, project } = props;
if (!auth.uid) return <Redirect to='/signin' />
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
// content here
</div>
<button onClick={(e) => handleClick(e, props.id)}>Delete</button>
</div>
</div>
)
} else {
return (
<div className="container center">
<p>Loading...</p>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => {
const id = ownProps.match.params.id;
const projects = state.firestore.data.projects;
const project = projects ? projects[id] : null
return {
project: project,
id: id,
auth: state.firebase.auth
}
}
const matchDispatchToProps = (dispatch) => {
return {
deleteProject: (id) => dispatch(deleteProject(id))
}
}
export default compose(
connect(mapStateToProps, matchDispatchToProps),
firestoreConnect([
{ collection: 'projects' }
])
)(ProjectDetails)
export const deleteProject = (id) => {
console.log("dispatch", id) \\ successfully shows "dispatch", id
return(dispatch, getState, {getFirestore}) => {
const firestore = getFirestore();
firestore.collection('projects').doc(id).delete()
.then(() => {
console.log('deleted') \\ does not show deleted here
dispatch({ type: 'DELETE_PROJECT_SUCCESS' });
}).catch(err => {
dispatch({ type: 'DELETE_PROJECT_ERROR' });
})
}
}
You are calling the imported version of deleteProject rather than the mapDispatchToProps version. This is a common gotcha.
One way to fix this (and prevent it happening in future) is to rename your action in your mapDispatchToProps to something different:
const matchDispatchToProps = (dispatch) => {
return {
dispatchDeleteProject: (e, id) => {
e.preventDefault()
dispatch(deleteProject(id))
})
}
}
Then you can destructure this out of your props and call it:
const ProjectDetails = (props) => {
const { auth, project, dispatchDeleteProject } = props;
if (!auth.uid) return <Redirect to='/signin' />
if (project) {
return (
<div className="container section project-details">
<div className="card z-depth-0">
// content here
</div>
<button onClick={e=>dispatchDeleteProject(e, props.id)}>Delete</button>
</div>
</div>
)
}
This is happening because your action deleteProject is not getting called from redux's dispatch.
If you will observer correctly, in your handleClick function, you are calling deleteProject function function action directly.
handleClick function should call deleteProject function from prop like this.
Your handleClick function should be -
const handleClick = (e, id, deleteProject) => { // passing deleteProject function from prop
e.preventDefault()
deleteProject(id)
console.log(id)
}
You HTML should be -
<button onClick={(e) => handleClick(e, props.id, props.deleteProject)}>Delete</button>