I'm working on a news aggregator, and I have a Newsfeed component that maps through the relevant posts and creates a Post component for each one. There's a Sidebar component that shows the user the feeds they are subscribed to, and allows them to subscribe to new ones or unsubscribe from existing ones. What I'd like to happen is:
When a user adds a new feed, Newsfeed rerenders and now shows posts from the new feed.
When a user removes a feed, Newsfeed rerender and no longer shows posts from that particular feed.
As far as retrieving the correct posts - my backend takes care of that, and it works fine. The backend returns posts based on the feeds that the user is subscribed to. The problem is, when the user adds or removes a feed, the Newsfeed component does not rerender, and requires a page reload to show the updated feed. At the same time however, the Redux store IS updated, and I can see the state change every time via the Redux Dev Tools.
In Newsfeed, I'm using the useSelector hook to get a few different pieces of state, yet the component does not rerender when the state changes. I was under the impression that any component that used the useSelector hook would automatically be rerendered when that piece of state changed, but if that's not how the hook works then please correct me.
Newsfeed.tsx:
import React, { useState, useRef, useCallback } from "react";
import usePostFetch from "../hooks/usePostFetch";
import { Post } from "./Post";
import { Tag } from "./Tag";
import { Upvote } from "./Upvote";
import { getDate } from "../services/getDate";
import { useSelector } from "react-redux";
import { InitialState } from "../store/reducers/rootReducer";
export const Newsfeed = (props: any) => {
const userState = useSelector((state: InitialState) => {
return state.auth;
});
const { user } = userState;
const publisherState = useSelector((state: InitialState) => {
return state.publishers.publishers;
});
const [pageNumber, setPageNumber] = useState(1);
const { loading, error, posts, hasMore } = usePostFetch(pageNumber);
const observer: any = useRef();
const lastPostElementRef = useCallback(
(node) => {
if (loading) return;
if (observer && observer.current) observer.current.disconnect();
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
console.log("Visible ");
setPageNumber((prevPageNumber) => prevPageNumber + 1);
}
});
if (node) observer.current.observe(node);
console.log(node);
},
[loading, hasMore]
);
return (
<div className="container mx-auto bg-gray-900" id="newsfeed">
<div className="object-center grid grid-cols-1 gap-8 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 mx-auto pb-6 pt-6">
{posts.map((post, index) => {
return (
<Post
key={post.id}
title={post.title}
url={post.url}
image={post.image}
category={post.category}
postId={post._id}
created={post.created}
publisher={post.publisher}
upvotes={post.upvotes}
/>
);
})}
</div>
<div>{loading && "Loading..."}</div>
<div>{error && "Error"}</div>
</div>
);
};
publisherActions.ts: (Relevant parts)
export const removeFeed = (allFeeds: any, feedName: any, userId: any) => async (
dispatch: Dispatch<PublisherDispatchTypes>
) => {
try {
axios({
method: "PUT",
url: "users/removepublisher",
params: { publisher: feedName, userId },
})
.then((res) => {
let newAllFeeds = allFeeds.filter((feed: any) => {
return feed.name.localeCompare(feedName) !== 0;
});
allFeeds = newAllFeeds;
console.log(`Feed was removed, ${res}`);
dispatch({
type: REMOVE_FEED_SUCCESS,
payload: allFeeds,
});
})
.catch((err) => {
console.log(`Error removing feed, ${err}`);
dispatch({
type: REMOVE_FEED_FAILURE,
});
});
} catch {
dispatch({ type: REMOVE_FEED_FAILURE });
console.log("Caught error while removing feed");
}
};
export const addFeed = (allFeeds: any, feed: any, userId: any) => async (
dispatch: Dispatch<PublisherDispatchTypes>
) => {
console.log("IN THE ADD_FEED FUNCTION");
try {
axios({
method: "PUT",
url: "users/addpublisher",
params: { publisher: feed, userId },
})
.then((res) => {
console.log(`Feed was added, ${res}`);
dispatch({
type: ADD_FEED_SUCCESS,
payload: {
name: feed.name,
url: feed.url,
image: feed.image,
},
});
})
.catch((err) => {
console.log(`Error adding feed, ${err}`);
dispatch({
type: ADD_FEED_FAILURE,
});
});
} catch {
dispatch({ type: ADD_FEED_FAILURE });
console.log("Caught error while adding feed");
}
publisherReducer.ts: (Relevant parts)
import { Reducer } from "react";
import {
PublisherDispatchTypes,
REMOVE_FEED_SUCCESS,
REMOVE_FEED_FAILURE,
ADD_FEED_SUCCESS,
ADD_FEED_FAILURE,
} from "../actions/publisherActionsTypes";
import { Publisher } from "../../../../shared/Publisher";
interface PublisherResponse {
publishers: Publisher[];
}
export interface PublisherState {
publishers: Publisher[] | undefined;
loadedUsersFeeds: boolean;
feedCount: number;
}
const defaultState: PublisherState = {
publishers: undefined,
loadedUsersFeeds: false,
feedCount: 0,
};
const publisherReducer = (
state: PublisherState = defaultState,
action: PublisherDispatchTypes
) => {
switch (action.type) {
case REMOVE_FEED_SUCCESS:
return {
...state,
publishers: action.payload,
};
case REMOVE_FEED_FAILURE:
return state;
case ADD_FEED_SUCCESS:
let pubs = state.publishers || [];
return {
...state,
publishers: [...pubs, action.payload],
};
case ADD_FEED_FAILURE:
return state;
default:
return state;
}
};
export default publisherReducer;
Related
I have this component (associated with the route /product/:id in App.js)
import React, { useEffect} from 'react';
import { useSelector, useDispatch } from 'react-redux';
import Spinner from '../components/Spinner';
import { getProductDetails } from '../actions/productActions';
const ProductDetailsScreen = ({ match }) => {
const { product, loading, error } = useSelector(
(state) => state.productDetails
);
console.log(product);// < ------- This outputs the previously display product, then undefined, then the targeted product
const dispatch = useDispatch();
useEffect(() => {
dispatch(getProductDetails(match.params.id));
}, [match]);
return (
<div>
<div className='container'>
{loading ? (
<Spinner />
) : error ? (
<h1>{error}</h1>
) : (
<div>
<h1>{product.name}</h1>
<span>
<strong>Price: </strong>${product.price}
</span>
</div>
)}
</div>
<div>
);
};
export default ProductDetailsScreen;
In another component I have this
<Link to={`/product/${_id}`}>View Details</Link>
which is supposed to go to ProductDetailsScreen component, and fill the screen with the product's details based on the _id passed. However, although the redux state is populated correctly from the backend with the product's details whose _id is passed, the components' elements aren't filled with the product's details as it is supposed to be, although I am checking if the product is done loading and there is no error. The component seems to be rendered 3 times based on the console.log(product). The first time it outputs the previously displayed product (I think I need to clear the state), then undefined, and then the target product!
Why? What am I doing wrong?
EDIT1:
the reducer
export const productDetailsReducer = (state = { product: {} }, action) => {
const { type, payload } = action;
switch (type) {
case PRODUCT_GET_REQUEST:
return { loading: true };
case PRODUCT_GET_SUCCESS:
return { loading: false, success: true, product: payload };
case PRODUCT_GET_FAIL:
return {
loading: false,
error: payload,
};
case PRODUCT_GET_RESET:
return { product: {} };
default:
return state;
}
};
and the action
export const getProductDetails = (productId) => async (dispatch) => {
try {
dispatch({
type: PRODUCT_GET_REQUEST,
});
const { data } = await axios.get(`/api/products/${productId}`);
dispatch({
type: PRODUCT_GET_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PRODUCT_GET_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
Please try below.
Reducer
export const productDetailsReducer = (state = { product: {} }, action) => {
const { type, payload } = action;
switch (type) {
case PRODUCT_GET_REQUEST:
return { ...state, loading: true };
case PRODUCT_GET_SUCCESS:
return { ...state, loading: false, success: true, product: payload };
case PRODUCT_GET_FAIL:
return {
...state,
loading: false,
error: payload
};
case PRODUCT_GET_RESET:
return { ...state, product: {} };
default:
return state;
}
};
I have a little problem.
I have diferent reducers in different files using a combine reducer, but when i try to use the "different"
INITIAL STATES on these reducers it doesnt apear
For example
Product Reducer -> This is the state that i have to take
const INITIAL_STATE = {
productosInventario: [],
loading: false,
error: ''
Category Reducer -> this is the state for these reducer
const INITIAL_STATE = {
categorias: [],
categoriaActual: '',
loading: false,
error: ''
}
The idea is use both on these component:
Component:
import React, { Component } from 'react'
/* Components */
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import CardItemInventario from '../components/inventario/CardItemInventario'
import * as ProductoActions from '../actions/ProductoActions'
import * as CategoriasActions from '../actions/CategoriasActions'
/* Styles */
import Spinner from '../components/Spinner'
import Fatal from '../components/Fatal'
import '../assets/styles/Containers/Inventario.scss'
class Inventario extends Component {
async componentDidMount() {
await this.props.traerTodosLosProductos();
}
handleChangeCategoria = (e) => {
this.props.cambioCategoriaInventario(e.target.value)
this.props.traerProductosPorCategoriaInventario(e.target.value)
}
/* Mapea todas las categorias disponibles en base de datos */
traerCategoriasInventario = () => this.props.categoriasInventario.map(category => {
let categori = category.categoria
return (
<option
value={categori}
>
{categori}
</option>
)
})
ponerContenido = () => {
if (this.props.loading) {
return (
<Spinner />
)
}
if (this.props.error) {
return (
<Fatal
error={this.props.error} />
)
}
return (
<>
<div className="button-add__cont">
<h1 className="button-add__title">
Inventario
</h1>
<Link to='/agregarinventario' className="button-add__cont--link">
Agregar a Inventario
</Link>
</div>
<select
name="categoriaSelect"
id=""
onChange={this.handleChangeCategoria}
className="selector-categoria"
>
<option value='' defaultValue> - Categoria -</option>
{this.traerCategoriasInventario()}
</select>
<div className="inventario-cont">
{this.imprimirProductos()}
</div>
</>
)
}
imprimirProductos = () => this.props.productosInventario.map(Productos =>
<CardItemInventario
nombre={Productos.nombre}
marca={Productos.marca}
cantidad={Productos.cantidad}
distribuidor={Productos.distribuidor}
precio={Productos.precio}
/>
)
render() {
console.log(this.props)
return (
<>
{this.ponerContenido()}
</>
)
}
}
const mapStateToProps = (reducers) => {
return (
reducers.ProductoReducer,
reducers.CategoriasReducer
)
}
const mapDispatchToProps = {
...ProductoActions,
...CategoriasActions
}
export default connect(mapStateToProps, mapDispatchToProps)(Inventario);
actions ->
productoActions:
import axios from 'axios'
import {
TRAER_TODOS_LOS_PRODUCTOS
} from '../types/ProductoTypes'
import { host_name, port_redux } from '../../../config'
import { CARGANDO, ERROR } from '../types/GlobalTypes'
const axiosConf = {
baseURL: `http://${host_name}:${port_redux}`
}
export const traerTodosLosProductos = () => async (dispatch) => {
dispatch({
type: CARGANDO
})
try {
const res = await axios.get(`/api/productos/get/listar`, axiosConf)
dispatch({
type: TRAER_TODOS_LOS_PRODUCTOS,
payload: res.data
})
} catch (error) {
console.log("Error: " + error)
dispatch({
type: ERROR,
payload: error.message
})
}
}
export const traerProductosPorCategoriaInventario = (categoria) => async (dispatch) => {
try {
const res = await axios.get(`/api/cotizacion/get/productosporcategoria/${categoria}`, axiosConf)
dispatch({
type: TRAER_TODOS_LOS_PRODUCTOS,
payload: res.data
})
} catch (error) {
console.log("Error: " + error)
dispatch({
type: ERROR,
payload: error.message
})
}
}
categoryActions_ >
import axios from 'axios'
import { host_name, port_redux } from '../../../config'
import { CARGANDO, ERROR } from '../types/GlobalTypes'
import {
LISTAR_CATEGORIAS,
CATEGORIA_ACTUAL
} from '../types/CategoriasTypes'
const axiosConf = {
baseURL: `http://${host_name}:${port_redux}`
}
export const traerCategoriasInventario = () => (dispatch) => {
const res = axios.get(`/api/categorias/get/listar`, axiosConf)
console.log(res)
dispatch({
type: LISTAR_CATEGORIAS,
payload: res.data.data
})
}
export const cambioCategoriaInventario = (categoria) => async (dispatch) => {
try {
dispatch({
type: CATEGORIA_ACTUAL,
payload: categoria
})
} catch (error) {
console.log("Error: " + error)
dispatch({
type: ERROR,
payload: error.message
})
}
}
const mapStateToProps = (reducers) => {
return (
reducers.ProductoReducer,
reducers.CategoriasReducer
)
}
It seems like you are having some confusion between state and reducer. The state is the object which contains all of your data. It is just a plain javascript object. The reducer is a function which takes the state object and an action and returns a new state object.
Your setup should look something like this:
const productoReducer = (state = INITIAL_PRODUCTOS, action ) => {
switch ( action.type ) {
case 'TRAER_TODOS_LOS_PRODUCTOS':
/* ... code here ... */
default:
return state;
}
}
const categoriasReducer = (state = INITIAL_CATEGORIAS, action ) => {
switch ( action.type ) {
case 'LISTAR_CATEGORIAS':
/* ... code here ... */
default:
return state;
}
}
export const reducer = combineReducers({
producto: productoReducer,
categorias: categoriasReducer,
})
Here we have two separate reducers for categories and for products, and each gets a separate initial state. We use combineReducers to put them together so now the combined state has properties producto and categorias.
Your component Inventario needs to access a bunch of values from state: categoriasInventario, productosInventario, loading, and error. Rather than passing the state into the component, we use mapStateToProps to extract these values and pass them as props.
const mapStateToProps = (state) => {
return {
categoriasInventario: state.categorias.categorias,
productosInventario: state.productos.productosInventario,
loading: state.categorias.loading || state.productos.loading,
error: state.categorias.error || state.productos.error,
}
}
I've been trying to create this search app where I can display the items in a table and delete items using react redux. However, on the initial load, the app shows a table but there is no data in the table. It's an empty table. If i search for another movie name which have more than one movie for that search term, then 2 tables would be shown but I want to show everything on the same table itself. The delete button is not working as well. Is there something wrong with my action and reducer files?
Action.js
import {
FETCH_MOVIE_PENDING,
FETCH_MOVIE_SUCCESS,
FETCH_MOVIE_ERROR,
DELETE_MOVIE
} from "./types";
const fetchMoviePendig = () => ({
type: FETCH_MOVIE_PENDING
});
const fetchMovieSuccess = json => ({
type: FETCH_MOVIE_SUCCESS,
payload: json
});
const fetchMovieError = error => ({
type: FETCH_MOVIE_ERROR,
payload: error
});
export const fetchMovie = name => {
return async dispatch => {
dispatch(fetchMoviePendig());
try {
const url = `https://jsonmock.hackerrank.com/api/movies/search/?Title=${name}`;
const response = await fetch(url);
const result = await response.json(response);
console.log(result);
dispatch(fetchMovieSuccess(result.data));
} catch (error) {
dispatch(fetchMovieError(error));
}
};
};
export const deleteEvent = id => async dispatch => {
try {
dispatch({
type: DELETE_MOVIE,
payload: id
});
} catch (err) {
console.log(err);
}
};
Reducer
import {
FETCH_MOVIE_PENDING,
FETCH_MOVIE_SUCCESS,
FETCH_MOVIE_ERROR,
DELETE_MOVIE
} from "../action/types";
const initialState = {
data: [],
loading: false,
error: ""
};
const moviesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_MOVIE_PENDING:
return {
...state,
loading: true
};
case FETCH_MOVIE_SUCCESS:
return {
...state,
loading: false,
data: [...state.data, action.payload]
};
case FETCH_MOVIE_ERROR:
return {
...state,
loading: false,
error: action.payload
};
case DELETE_MOVIE:
return {
...state,
data: state.data.filter(movie => movie.id !== action.payload)
};
default:
return state;
}
};
export default moviesReducer;
App.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { fetchMovie } from "./action/movieActions";
import Input from "./components/Input";
import MovieTable from "./components/MovieTable";
class App extends Component {
state = {
searchInput: "The Rain"
};
componentDidMount() {
this.props.getMovieList(this.state.searchInput);
}
_getMovie = () => {
this.props.getMovieList(this.state.searchInput);
};
_onChangeHandler = e => {
this.setState({
searchInput: e.target.value
});
console.log(this.state.searchInput);
};
render() {
const { data, loading } = this.props.movies;
return (
<div className="center">
<div>
<h2 className="center white-text">Movie Search</h2>
</div>
<div className="container">
<Input
value={this.state.searchInput}
onChange={this._onChangeHandler}
onClick={this._getMovie}
/>
<div className="row">
{loading ? (
<p>Loading</p>
) : (
data.map(item => (
<MovieTable
key={item.imdbID}
year={item.Year}
name={item.Title}
movieId={item.imdbId}
/>
))
)}
</div>
</div>
</div>
);
}
}
const mapStateToProps = state => {
return {
movies: state.movies
};
};
const mapDispatchToProps = dispatch => {
return {
getMovieList: name => dispatch(fetchMovie(name))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
Hello please take a look at the sandbox : https://codesandbox.io/s/prod-wind-4hgq2?file=/src/App.js
I have edited
<MovieTable
data={data.map(d => ({
year: d.Year,
name: d.Title,
movieId: d.imdbId
}))}
/>
and
case FETCH_MOVIE_SUCCESS:
return {
...state,
loading: false,
data: action.payload
};
And ... Currently the delete button has no event, that's why it can't work
I see data having the following pattern:
Object {page: 1, per_page: 10, total: 1, total_pages: 1, data: Array[1]}
page: 1
per_page: 10
total: 1
total_pages: 1
data: Array[1]
0: Object
Title: "Sin in the Rain"
Year: 2006
imdbID: "tt1072449"
And you are accessing wrong properties in the component render logic, can you fix that.
Duplicate table is created the way you have written the logic.
Pass the data to MovieTable component and let it render and create the table
and fill it.
In reducer (FETCH_MOVIE_SUCCESS) you need don't need to append data you have to
replace or use the current movie data only.
I have a post details component where on clicking the like button the redux state changes the redux state is like
posts
->postDetails
I'am changing the liked property and number of likes of postDetais object, On clicking the like button the liked property is set to true from false and vice versa and the number of likes is incremented.
However the state is changing but the componentDidUpdate method is not firing
PostDetails.js
import React, { Component } from "react";
import { connect } from "react-redux";
import {
getPostData,
likePost,
unlikePost
} from "../../store/actions/postsActions";
import { Icon, Tooltip } from "antd";
import { Link } from "react-router-dom";
export class PostDetails extends Component {
state = {
postData: this.props.postDetails
};
componentDidMount() {
this.props.getPostData(this.props.match.params.post_id);
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(this.props.postDetails);
if (prevProps.postDetails !== this.props.postDetails) {
this.setState({
postData: this.props.postDetails
});
}
}
render() {
const { postData } = this.state;
const liked = postData.liked;
return (
<div className="postDetails">
{postData && (
<div className="postDetailsContainer">
<div className="postImage">
<img src={postData.imageUrl} alt={postData.caption} />
</div>
<div className="postContent">
<div className="postContent__header">
<Link
to={`/user/${postData.username}`}
className="postContent__headerContent"
>
<img
src={postData.profileUrl}
alt={postData.username}
className="postContent__profileImage"
/>
<p className="postContent__username">{postData.username}</p>
</Link>
</div>
<div className="postComments" />
<div className="postInfo">
<div className="postActions">
{liked ? (
<Tooltip title="Unlike post">
<Icon
type="heart"
className="likePost"
theme="filled"
style={{ color: "#d41c00" }}
onClick={() => this.props.unlikePost(postData.id)}
/>
</Tooltip>
) : (
<Tooltip title="Like post">
<Icon
type="heart"
className="likePost"
onClick={() => this.props.likePost(postData.id)}
/>
</Tooltip>
)}
<Tooltip title="Comment">
<Icon type="message" className="commentButton" />
</Tooltip>
</div>
<Tooltip title="Refresh comments">
<Icon type="reload" className="reloadComments" />
</Tooltip>
</div>
<div />
</div>
</div>
)}
</div>
);
}
}
const mapStateToProps = state => {
return {
postDetails: state.posts.postDetails
};
};
const mapDispatchToProps = dispatch => {
return {
getPostData: postId => dispatch(getPostData(postId)),
likePost: postId => dispatch(likePost(postId)),
unlikePost: postId => dispatch(unlikePost(postId))
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(PostDetails);
postsReducer.js
const initialState = {
creatingPost: false,
feed: [],
createdPost: false,
feedUpdated: false,
postDetails: {}
};
const postsReducer = (state = initialState, action) => {
switch (action.type) {
case "CREATING_POST":
return {
...state,
creatingPost: true,
createdPost: false
};
case "ADD_POST":
return {
...state,
feed: state.feed.concat(action.payload)
};
case "FETCH_FEED":
return {
...state,
feed: action.payload
};
case "CREATED_POST":
return {
...state,
creatingPost: false,
createdPost: true
};
case "UPDATE_FEED":
return {
...state,
feed: action.payload,
feedUpdated: true
};
case "GET_POST_DATA":
return {
...state,
postDetails: action.payload
};
case "RESET_FEED_UPDATED":
return {
...state,
feedUpdated: false
};
case "RESET_CREATED_POST":
return {
...state,
createdPost: false
};
case "LIKED_POST":
const { postDetails } = state;
postDetails.liked = true;
postDetails.likes += 1;
return {
...state,
postDetails: postDetails
};
case "UNLIKED_POST":
const postDetails1 = state.postDetails;
postDetails1.liked = false;
postDetails1.likes -= 1;
return {
...state,
postDetails: postDetails1
};
case "CLEAR_POST_DATA":
return initialState;
default:
return state;
}
};
export default postsReducer;
postsActions.js
import Axios from "axios";
import moment from "moment";
import store from "../store";
export const createPost = postData => {
return (dispatch, getState) => {
dispatch({ type: "CREATING_POST" });
Axios.post("/api/post/new", {
imageUrl: postData.imageUrl,
caption: postData.caption
})
.then(res => {
dispatch({ type: "CREATED_POST" });
dispatch({ type: "ADD_POST", payload: res.data.post });
})
.catch(err => {
console.log(err);
});
};
};
export const fetchFeed = () => {
return (dispatch, getState) => {
Axios.get("/api/user/feed")
.then(res => {
var feed = res.data.feed;
const state = store.getState();
const likedPosts = state.user.userData.likedPosts;
for (var i = 0; i < feed.length; i++) {
for (var j = 0; j < feed.length - i - 1; j++) {
if (moment(feed[j + 1].createdAt).isAfter(feed[j].createdAt)) {
var temp = feed[j];
feed[j] = feed[j + 1];
feed[j + 1] = temp;
}
}
}
for (var i = 0; i < feed.length; i++) {
if (likedPosts.indexOf(feed[i]._id) > -1) {
feed[i]["liked"] = true;
} else {
feed[i]["liked"] = false;
}
}
console.log(feed);
dispatch({ type: "FETCH_FEED", payload: feed });
})
.catch(err => {
console.log(err);
});
};
};
export const likePost = postId => {
return (dispatch, getState) => {
Axios.put("/api/post/like", { postId: postId })
.then(res => {
const feed = store.getState().posts.feed;
feed.forEach(post => {
if (post._id === postId) {
post.liked = true;
}
});
dispatch({ type: "UPDATE_FEED", payload: feed });
dispatch({ type: "LIKED_POST", payload: res.data.postId });
})
.catch(err => {
console.log(err);
});
};
};
export const unlikePost = postId => {
return (dispatch, getState) => {
Axios.put("/api/post/unlike", { postId: postId })
.then(res => {
const feed = store.getState().posts.feed;
feed.forEach(post => {
if (post._id === postId) {
post.liked = false;
}
});
dispatch({ type: "UPDATE_FEED", payload: feed });
dispatch({ type: "UNLIKED_POST", payload: res.data.postId });
})
.catch(err => {
console.log(err);
});
};
};
export const getPostData = postId => {
return (dispatch, getState) => {
Axios.get(`/api/post/${postId}`)
.then(res => {
const likedPosts = store.getState().user.userData.likedPosts;
if (likedPosts.indexOf(postId) > -1) {
res.data.post["liked"] = true;
} else {
res.data.post["liked"] = false;
}
dispatch({ type: "GET_POST_DATA", payload: res.data.post });
})
.catch(err => {
console.log(err);
});
};
};
export const resetFeedUpdated = () => {
return (dispatch, getState) => {
dispatch({ type: "RESET_FEED_UPDATED" });
};
};
export const resetCreatedPost = () => {
return (dispatch, getState) => {
dispatch({ type: "RESET_CREATED_POST" });
};
};
Your LIKED_POST and UNLIKED_POST reducer cases are not pure - they are are mutating the existing postDetails object in the state and putting it back into state so connect is optimizing and not re-rendering when it does a shallow equals comparison on postDetails from the previous and next props in componentShouldUpdate. Make sure you're creating a completely new value for postDetails like:
case "LIKED_POST":
const { postDetails } = state;
const newPostDetails = {
...postDetails,
liked: true,
likes: postDetails.likes + 1,
};
return {
...state,
postDetails: newPostDetails
};
You should check, if the comparison if (prevProps.postDetails !== this.props.postDetails) ever hits. Because with the like function you only change properties of the same object, the comparison will fail, because it's still the same object reference for postDetails. Try to return a new object in your reducer:
case "LIKED_POST":
const { postDetails } = state;
postDetails.liked = true;
postDetails.likes += 1;
return {
...state,
postDetails: {
...postDetails
},
}
Also if you're not changing anything of the object inside the component but in Redux store why not use the component property directly? You can remove the state object and the componentDidUpdate. Also you could refactor it to a function component.
render() {
const { postDetails: postData } = this.props;
...
}
When working with Redux, never forget the three principles
Single Source of truth
State is ready only
Reducers must be pure functions: Reducers take previous state and some action and modifies it and returns new state. We should never mutate state. We should create new objects and return them.
You have mutated existing state in your reducer functions. This doesnt trigger componentdidupdate because, connect method ( it checks mapStateToProps) treats that there is nothing that changed (It checks reference and since reference didnt change Component is not invoked).
You can use Object.assign or use spread operator which helps to make your reducers return a new object.
Change your Liked and unlinked posts reducer functions to return a new object instead of mutating existing object.
#azundo added how your code should be to achieve what you need.
I'm building a simple CRUD note app and I'm having issues getting the child components to update after simple POST and DELETE api calls to create and delete notes.
Here's the parent component with a simple form and a child component <NotepadsShowView /> to render the submitted notes.
class AuthenticatedHomeView extends Component {
_handleSubmit(e) {
e.preventDefault()
const { dispatch } = this.props
const data = {
title: this.refs.title.value,
description: this.refs.description.value,
private: this.refs.private.checked
}
dispatch(Actions.createNotepad(this.props.currentUser.id, data))
this._resetForm()
}
_resetForm() {
this.refs.title.value = ''
this.refs.description.value = ''
this.refs.private.checked = true
}
render() {
return (
<div>
<form onSubmit={::this._handleSubmit}>
{/* form removed for clarity */}
</form>
<NotepadsShowView/>
</div>
)
}
}
and the NotepadsShowView child component:
class NotepadsShowView extends Component {
componentWillMount() {
const { dispatch, currentUser } = this.props
dispatch(Actions.fetchNotepads(currentUser.id))
}
_renderEachOwnedNotepad() {
const { ownedNotepads } = this.props
return ownedNotepads.map((notepad, i) => {
return <NotepadShowView key={notepad.id} {...notepad} {...this.props}/>
})
}
render() {
const { isFetchingNotepads } = this.props
const notepads = this._renderEachOwnedNotepad()
if (isFetchingNotepads || notepads.length == 0) return null
return (
<ul className="notepads-container">
{notepads}
</ul>
)
}
}
const mapStateToProps = (state) => ({
isFetchingNotepads: state.notepads.fetching,
currentUser: state.session.currentUser,
ownedNotepads: state.notepads.ownedNotepads,
sharedNotepads: state.notepads.sharedNotepads
})
export default connect(mapStateToProps)(NotepadsShowView)
Here is the action creator:
const Actions = {
createNotepad: (userId, data) => {
return dispatch => {
httpPost(`/api/v1/users/${userId}/notepads`, {data: data})
.then(data => {
dispatch({
type: CONSTANTS.NOTEPADS_CREATED,
notepad: data
})
})
.catch(error => {
error.response.json()
.then(json => {
dispatch({
type: CONSTANTS.NOTEPADS_CREATE_ERROR,
errors: json.errors,
})
})
})
}
},
fetchNotepads: (userId) => {
return dispatch => {
dispatch({
type: CONSTANTS.NOTEPADS_FETCHING
})
httpGet(`/api/v1/users/${userId}/notepads`, {id: userId})
.then(data => {
dispatch({
type: CONSTANTS.NOTEPADS_RECEIVED,
notepads: data.notepads
})
})
.catch(error => {
error.response.json()
.then(json => {
dispatch({
type: CONSTANTS.NOTEPADS_ERRORS,
errors: json.error
})
})
})
}
},
deleteNotepad: (userId, notepadId) => {
return dispatch => {
httpDelete(`api/v1/users/${userId}/notepads/${notepadId}`, {id: notepadId})
.then(data => {
dispatch({
type: CONSTANTS.NOTEPADS_OWNED_DELETE,
id: notepadId
})
})
}
},
}
Here is the reducer:
const initialState = {
ownedNotepads: [],
fetching: true,
}
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case CONSTANTS.NOTEPADS_FETCHING:
return {
...state,
fetching: true,
}
case CONSTANTS.NOTEPADS_RECEIVED:
return {
...state,
fetching: false,
ownedNotepads: action.notepads
}
case CONSTANTS.NOTEPADS_CREATED:
return {
...state,
ownedNotepads: [
...state.ownedNotepads,
{
id: action.id,
title: action.title,
description: action.description,
private: action.private
}
]
}
case CONSTANTS.NOTEPADS_OWNED_DELETE:
const index = state.ownedNotepads.findIndex(note => note.id === action.id)
return {
...state,
ownedNotepads: [
...state.ownedNotepads,
state.ownedNotepads.slice(0, index),
state.ownedNotepads.slice(index + 1)
]
}
default:
return state
}
}
A user submits a new notepad which triggers an POST api call. Server returns the new notepad and the reducer adds the notepad to the Redux state. No issues here. However, when a notepad is created the notepad props are undefined and no new notepads are being shown in the child UI components. They don't know of the update and I assume it's because I'm not handling the state update.
I am using componentWillMount (cWM) above to fetch the updated notepads state before the initial render. I'm assuming I should use componentWillReceiveProps but I understand there will be an infinite loop if I dispatch the fetchNotepads action because the dispatch in cWM will run again.
My question is how do I update the component props when the Redux state changes? Do I have to use component state? What about the lifecycle methods?