Search code examples
javascriptreactjsnode.jsreduxredux-toolkit

Unable to Fetch Posts by Search Criteria in React Application Using Redux and Express.js Backend


I'm building a React application with Redux for state management and an Express.js backend for handling API requests. I'm trying to implement a search functionality to fetch posts based on search criteria entered by the user. However, I'm encountering an issue no posts are fetched. It returns this JSON data instead of posts with specified search query

const useQuery = () => {
    return new URLSearchParams(useLocation().search)
}

export default function Home() {
    const classes = useStyles()
    const dispatch = useDispatch();

    const [search, setSearch] = useState('')
    const [tags, setTag] = useState([])
    const [currentId, setCurrentId] = useState(null)

    const liked = useSelector((state) => state.posts.isLiked)
    const del = useSelector((state) => state.posts.isDeleted)

    const query = useQuery()
    const navigate = useNavigate()

    const page = query.get('page') || 1
    const searchQuery = query.get('searchQuery')

    useEffect(() => {
        console.log('Effect triggered');
        dispatch(getPosts())
    }, [currentId, dispatch, liked, del])

    console.log(search)

    const searchingPost = () => {
        if (search.trim() || tags.length > 0) {
            dispatch(searchPost({ search, tags: tags.join(',') }))
            console.log("this is in in the search",search)
        } else {
            navigate('/')
        }
    }

    const handleKeyPress = (e) => {
        console.log("outside the if condition")
        if (e.keyCode === 'Enter') {
            searchingPost()
            console.log('inside the handlekeypress')
        }
    }

    const handleChange = (newChips) => {
        setTag(newChips)
    }

    const handleAdd = (tag) => {
        setTag([...tags, tag])
    }

    const handleDelete = (tagToDelete) => {
        setTag(tags.filter((tag) => tag !== tagToDelete))
    }

    return (
        <>
            <Grow in>
                <Container maxWidth='xl'>
                    <Grid container justify='space-between' alignItems='stretch' spacing={3} className={classes.gridContainer}>
                        <Grid item xs={12} sm={6} md={9}  >
                            <Posts currentId={currentId} setCurrentId={setCurrentId} />
                        </Grid>
                        <Grid item xs={12} sm={6} md={3}   >
                            <AppBar className={classes.appBar} position='static' color='inherit'>
                                <TextField
                                    name='search'
                                    label='Search Memories'
                                    fullWidth
                                    value={search}
                                    onChange={(e) => setSearch(e.target.value)}
                                    onKeyPress={handleKeyPress}
                                    variant='outlined'
                                />
                                <MuiChipsInput
                                    style={{ margin: '10px 0' }}
                                    value={tags}
                                    onChange={handleChange}
                                    // onAdd = {handleAdd}

                                    // onDelete = {handleDelete}
                                    label='Search Tags'
                                    variant='outlined'
                                ></MuiChipsInput>
                                <Button onClick={searchingPost} className={classes.searchButton} color='primary'>Search</Button>
                            </AppBar>
                            <Form currentId={currentId} setCurrentId={setCurrentId} />
                            <br />
                            <Paper className={classes.pagination} elevation={6}>
                                <Paginate />
                            </Paper>
                        </Grid>
                    </Grid>
                </Container>
            </Grow>
        </>
    )
}
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import {
  fetchPostsBySearch,
  fetchPosts,
  createPost as apiCreatePost,
  updatePost as apiUpdatePost,
  likePost,
  deletePost
} from '../api/index.js';

const initialState = {
    isDeleted: false,
    posts: [],
    searchResults : [],
    isLiked: false
}

const postSlice = createSlice({
    name: 'posts',
    initialState,
    reducers: {
        setPosts(state, action) {
            state.posts = action.payload;
        },

        searchPost(state,action) {
            state.searchResults = action.payload;
        },
        addPost(state, action) {
            state.posts = [...state.posts, action.payload]
        },
        update(state, action) {
            const updatedPost = action.payload;
            // Find the post in the state by ID and update it

            state.posts = state.posts.map(post => (post._id === updatedPost._id ? updatedPost : post));
            // if post id equal to updated post id return updatedpost else return the previous post
        },
        like(state, action) {
            const postliked = action.payload;
            // Find the post in the state by ID and update it

            state.posts = state.posts.map(post => (post._id === postliked._id ? postliked : post));
            // state.posts = [...state.posts];
            state.isLiked = true;
        },
        deletepost(state, action) {
            console.log(action.payload)

            state.posts = state.posts.filter((post) => post._id !== action.payload)  
            console.log('inside the reducer',state.posts)
            // state.posts = [...state.posts];
            state.isDeleted = true
        },
        resetFlags(state) {
            state.isDeleted = false;
            state.isLiked = false;
        }
    },
});

export const {
  setPosts,
  addPost,
  update,
  like,
  deletepost,
  resetFlags,
  searchPost
} = postSlice.actions;

export default postSlice.reducer;

export const getPosts = createAsyncThunk('posts/getPosts', async (_, { dispatch }) => {
    try {
        const { data } = await fetchPosts();
        dispatch(setPosts(data));
    } catch (error) {
        console.log(error.message);
    }
});

export const searchPosts = createAsyncThunk('posts/searchPosts', async ({searchQuery}, { dispatch }) => {
    try {
        const { data   } = await fetchPostsBySearch(searchQuery);
        dispatch(searchPost(data));
        console.log(data)
    } catch (error) {
        console.log(error.message);
    }
});

API index.js

export const fetchPostsBySearch = (searchQuery) => API.get(`/posts/search?searchQuery=${searchQuery.search || 'none'}&tags=${searchQuery.tags}`)

Query is actually reaching the backend but requesting i'm getting from backend it this '{search: 'Trek', tags: ''} [[Prototype]] '

export const getPostBySearch = async (req, res) => {
    const { searchQuery, tags } = req.query
    console.log(req.body)

    try {
        console.log(searchQuery)
        const title = new RegExp(searchQuery,'i') // i stands for ignore case
        const posts = await PostMessage.find({ $or: [{title}, {tags : { $in:  tags.split(',')}}]} )
        res.json({data : posts})

        console.log("this is for searchings posts",posts)
    } catch(error) {
        res.status(404).json({message : error.message })
    }
}

How to resolve this error I want to fetch posts based on their title, but response is returning nothing and it shows no error.


Solution

  • You have two very similarly named actions, searchPost and searchPosts. searchPost is the action that is dispatched to update the state with fetched "post(s)" as the action payload, and searchPosts is the Thunk action to initiate the network request. The UI code is dispatching the former instead of the latter, so state.searchResults is set to the value of { search, tags: tags.join(',') } from the dispatched searchPost action in searchingPost.

    const searchingPost = () => {
      if (search.trim() || tags.length > 0) {
        dispatch(searchPost({ search, tags: tags.join(',') }));
        console.log("this is in in the search", search);
      } else {
        navigate('/');
      }
    };
    
    searchPost(state, action) {
      state.searchResults = action.payload;
    },
    

    searchingPost should dispatch the searchPosts Thunk action instead.

    const searchingPost = () => {
      if (search.trim() || tags.length > 0) {
        dispatch(searchPosts({ search, tags: tags.join(',') }));
      } else {
        navigate('/');
      }
    };
    

    You are also not leveraging Redux-Toolkit well. Thunk action creators actually generate 3 separate actions, .pending, .fulfilled, and .rejected. Add these to the state slice's extraReducers instead of creating additional actions manually.

    Example:

    import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
    import {
      fetchPostsBySearch,
      fetchPosts,
      createPost as apiCreatePost,
      updatePost as apiUpdatePost,
      likePost,
      deletePost
    } from '../api/index.js';
    
    const initialState = {
      isDeleted: false,
      posts: [],
      searchResults : [],
      isLiked: false
    };
    
    export const getPosts = createAsyncThunk(
      'posts/getPosts',
      async (_, { rejectWithValue }) => {
        try {
          const { data } = await fetchPosts();
          return data; // <-- return fulfilled data
        } catch (error) {
          console.log(error.message);
          return rejectWithValue(error); // <-- return rejected value
        }
      },
    );
    
    export const searchPosts = createAsyncThunk(
      'posts/searchPosts',
      async ({ searchQuery }, { rejectWithValue }) => {
        try {
          const { data } = await fetchPostsBySearch(searchQuery);
          return data; // return fulfilled value
        } catch (error) {
          console.log(error.message);
          return rejectWithValue(error); // <-- return rejected value
        }
      }
    );
    
    const postSlice = createSlice({
      name: 'posts',
      initialState,
      reducers: {
        addPost(state, action) {
          state.posts = [...state.posts, action.payload]
        },
        update(state, action) {
          const updatedPost = action.payload;
    
          state.posts = state.posts.map(post => (post._id === updatedPost._id ? updatedPost : post));
        },
        like(state, action) {
          const postliked = action.payload;
    
          state.posts = state.posts.map(post => (post._id === postliked._id ? postliked : post));
          state.isLiked = true;
        },
        deletePost(state, action) {
          state.posts = state.posts.filter((post) => post._id !== action.payload)  
          state.isDeleted = true
        },
        resetFlags(state) {
          state.isDeleted = false;
          state.isLiked = false;
        }
      },
      extraReducers: builder => {
        builder
          .addCase(getPosts.fulfilled, (state, action) => {
            state.posts = action.payload;
          })
          .addCase(getPosts.rejected, (state, action) => {
            // store error for display, etc
          })
          .addCase(searchPosts.fulfilled, (state, action) => {
            state.searchResults = action.payload;
          })
          .addCase(searchPosts.fulfilled, (state, action) => {
            // store error for display, etc
          });
      },
    });
    
    export const {
      addPost,
      update,
      like,
      deletePost,
      resetFlags
    } = postSlice.actions;
    
    export default postSlice.reducer;