How to update component on state change in redux - reactjs

I am trying to update component on state change in redux. I have a list of images, when user deletes the image the component should update after deleting the targeted image.
I have tried using componentWillReceiveProps and componentDidUpdate life cycle method, but none is working here. Can someone please suggest me what I am doing wrong here?
what I have done so far
action
import { DELETE_GALLERY_SUCCESS, DELETE_GALLERY_FAIL} from "./types";
export const deleteGalleryImage = (id) => (dispatch, getState) => {
axios
.delete(`${baseURL}/api/aws/gallery/${id}/delete/`, tokenConfig(getState))
.then(res => {
dispatch({
type: DELETE_GALLERY_SUCCESS,
payload: res.data
});
})
.catch(err => {
dispatch(returnErrors(err.response.data, err.response.status));
dispatch({
type: DELETE_GALLERY_FAIL
});
});
};
types.js
export const DELETE_GALLERY_SUCCESS = "DELETE_GALLERY_SUCCESS"
export const DELETE_GALLERY_FAIL = "DELETE_GALLERY_FAIL"
Reducer
import {DELETE_GALLERY_SUCCESS, DELETE_GALLERY_FAIL} from "../actions/types";
const initialState = {
paginations: true,
isLoading: false,
gallery: [],
};
export default function (state = initialState, action) {
switch (action.type) {
case DELETE_GALLERY_SUCCESS:
return {
...state,
isLoading: false,
gallery: state.gallery.filter(gallery => gallery.id !== action.payload)
};
case DELETE_GALLERY_FAIL:
return {
...state,
isLoading: false
};
default:
return state;
}
}
Here is my component
import { getAllGalleryImages, deleteGalleryImage } from '../settings/../../actions/gallery';
class DeleteGalleryImage extends Component {
componentDidMount() {
this.props.getAllGalleryImages();
}
componentWillReceiveProps(nextProps) {
if (this.props.images !== nextProps.images) {
// This is not working.
// what life cycle method should I use for this scenario?
//this.props.getAllGalleryImages()
}
}
handleDelete = (id) => {
this.props.deleteGalleryImage(id)
}
render() {
return (
<Row>
<Col xs={24} sm={22} offset={1}>
<h1 className='sub-page-heading'><span className='common_dlt'>Delete</span> Gallery Image</h1>
<div className='masonry'>
{this.props.images && this.props.images.results && this.props.images.results.map(result =>
<div className='masonry-item' key={result.id}>
<img src={result.gallery_img_url} className='dlt_blg_img' alt='img' id={result.id} />
<span className='gallery_delete_zone' onClick={() => this.handleDelete(result.id)}><Icon type="delete" /></span>
</div>
)}
</div>
</Col>
</Row>
)
}
}
const mapStateToProps = state => ({
images: state.gallery
});
export default connect(
mapStateToProps,
{ getAllGalleryImages, deleteGalleryImage }
)(DeleteGalleryImage);
Store
import { createStore, applyMiddleware } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import thunk from "redux-thunk";
import rootReducer from "./reducers";
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
rootReducer
import { combineReducers } from "redux";
import gallery from './gallery';
export default combineReducers({
gallery,
});

In this case no need to use componentWillReceiveProps, If you can change the store succcesfully the component will be rendered automatically since it is connected to the store.
Another thing I noticed is in your deleteGalleryImage action, I think you need to send id input parameter as payload, because we cannot assume if res.data will be id.
dispatch({
type: DELETE_GALLERY_SUCCESS,
payload: id
})
Edit: based on the info I got from comments, I understood your initial state
const initialState = {
count: 2,
gallery: {},
isLoading: true,
next: null,
paginations: true,
previous: null,
results: [{}, {}]
};
So your DELETE_GALLERY_SUCCCESS case must be like this if you want to remove a image in results array.
case DELETE_GALLERY_SUCCESS:
return {
...state,
isLoading: false,
results: state.results.filter(image => image.id !== action.payload)
}
};

Related

Pass state from one reducer to another in Redux

I want to pass the state from my searchReducer to my movieReducer. The search takes in an input and saves the id of the movie(s), into state, I want to take that id value and pass it into the fetch for my movies, so that I can fetch each movie with the id and save the data into the movieReducer's state. How can I do this?
actions.js
// ------------------ SEARCH ------------------
export const searchMovie = text => dispatch => {
dispatch({
type: SEARCH_MOVIE,
payload: text
})
}
export const fetchSearch = text => dispatch => {
axios.get(`https://api.themoviedb.org/3/search/multi?api_key=API_KEY&language=en-US&query=${text}&page=1&include_adult=false`)
.then(response => dispatch({
type: FETCH_SEARCH,
payload: response.data.results.map(search => search.id)
}))
.catch(error => console.log(error))
}
// ------------------ MOVIES ------------------
export const fetchMovie = text => dispatch => {
axios.get(`https://api.themoviedb.org/3/movie/${text}?api_key=API_KEY&append_to_response=videos,credits,recommendations,watch/providers`)
.then(response => dispatch({
type: SPECIFIC_MOVIE,
payload: response.data.results
}))
.catch(error => console.log(error))
}
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
import App from './App'
import reportWebVitals from './reportWebVitals';
import favoritesReducer from './redux/favoritesReducer.js'
import moviesReducer from './redux/moviesReducer.js'
import showsReducer from './redux/showsReducer.js'
import userReducer from './redux/userReducer';
import searchReducer from './redux/searchReducer.js'
const rootReducer = combineReducers({
favorties: favoritesReducer,
movies: moviesReducer,
shows: showsReducer,
users: userReducer,
search: searchReducer
})
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
let store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
export default
searchReducer.js
const initialState = {
text: '',
movies: [],
loading: false,
movie: []
}
const searchReducer = (state = initialState, {type, payload}) => {
switch (type) {
case 'SEARCH_MOVIE':
return {
...state,
text: payload,
loading: false
};
case 'FETCH_SEARCH':
return {
...state,
movies: payload,
loading: false
};
default:
return state;
}
}
export default searchReducer
movieReducer.js
const initialState = {
text: '',
movie: []
}
const moviesReducer = (state = initialState, {type, payload}) => {
switch (type) {
case 'SPECIFIC_MOVIE':
return {
...state,
movie: payload
};
default:
return state;
}
}
export default moviesReducer
MoviePage.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchMovie } from '../../actions/searchActions';
export class Movie extends Component {
componentDidMount() {
this.props.fetchMovie(this.props.match.params.id);
}
render() {
const { movie } = this.props;
let movieInfo = (
<div className="container">
<img src={movie.Poster} className="thumbnail" alt="Poster" />
<h2 className="mb-4">{movie.Title}</h2>
<li>Genre:</li> {movie.Genre}
<li>Released:</li> {movie.Released}
<li>Rated:</li> {movie.Rated}
<li>IMDB Rating:</li> {movie.imdbRating}
<li>Director:</li> {movie.Director}
<li>Writer:</li> {movie.Writer}
<li>Actors:</li> {movie.Actors}
<h3>About </h3>
{movie.Plot}
</div>
);
return <div>{}</div>;
}
}
const mapStateToProps = state => ({
movie: state.movies.movie
});
export default connect(mapStateToProps,{ fetchMovie })(Movie);
You can access the current state tree of your application using getState method inside of your action creator.
export const fetchMovie = text => (dispatch, getState) => {
console.log(getState()); // you can see the info about your state tree here
axios.get(`https://api.themoviedb.org/3/movie/${text}?api_key=API_KEY&append_to_response=videos,credits,recommendations,watch/providers`)
.then(response => dispatch({
type: SPECIFIC_MOVIE,
payload: response.data.results
}))
.catch(error => console.log(error))
}

Actions must be plain objects. Use custom middleware for async actions Saga thunk I do have so far in my store

The problem is:
I'm trying to use redux-saga in my react app, but i still has this error: Actions must be plain objects. Use custom middleware for async actions. Code it seems correct but no idea why gives that error. I'll be glad for all the help. I'm fighting with it for about two days and still doesn't have a solution. I tried to look up, but I still have this error.
action...
import { GET_DISTRICTS} from '../../constants';
const getAdres = async (url) => {
let response = await fetch(url);
let data = await response.json();
let list = [];
data.AdresList.Adresler.Adres.forEach((item) => {
console.info(item);
list.push({
label: item.ADI,
value: item.ID
});
});
return list;
};
export const actions = {
handleGetDistrictsData: async () => {
let districts = await getAdres(`url is here`);
return {
type: GET_DISTRICTS,
payload: districts
};
},
reducer...
import { GET_DISTRICTS } from '../../constants';
export const initialState = {
districts: [],
quarters: [],
streets: [],
doors: [],
districtSelected: false,
districtSelectedID: null,
quarterSelected: false,
quarterSelectedID: null,
streetSelected: false,
streetSelectedID: null,
doorSelected: false,
doorSelectedID: null
};
export default (state = initialState, action) => {
switch (action.type) {
case GET_DISTRICTS:
return {
...state,
districts: action.payload
};
default:
return state;
}
};
component...
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { actions as addressActions } from '../../../../redux/actions/address';
import Select from 'react-select';
const Districts = (props) => {
let [ fetchedData, setFetchedData ] = useState(false);
useEffect(() => {
props.handleGetDistrictsData();
setFetchedData(true);
});
return (
<React.Fragment>
<Select
name='adresSelect'
options={props.address.districts}
onChange={props.handleDistrictChange}
placeholder='Please Select'
/>
</React.Fragment>
);
};
const mapStateToProps = (state) => ({
address: state.address
});
const mapDispatchToProps = function(dispatch) {
return bindActionCreators({ ...addressActions }, dispatch);
};
export default connect(mapStateToProps, mapDispatchToProps)(Districts);
-------------
import React from 'react';
import Districts from './Districts';
const AddressSearchWidget = (props) => {
return (
<React.Fragment>
<Districts />
</React.Fragment>
);
};
export default AddressSearchWidget
store...
import { applyMiddleware, combineReducers, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas/index';
import * as reducers from './';
export function initStore() {
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const rootReducer = combineReducers(reducers);
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, composeEnhancer(applyMiddleware(sagaMiddleware)));
// Run sagas
sagaMiddleware.run(rootSaga);
return store;
}
handleGetDistrictsData returns a promise (all async functions return promises). You cannot dispatch a promise in plain redux saga, and redux-saga does not change this. Instead, dispatch a normal action, and have that action run a saga. The saga can then do async things, and when it's done dispatch another action. The reducer listens only for that second action.
// Actions:
export const getDistrictsData = () => ({
type: GET_DISTRICTS,
})
export const districtsDataSuccess = (districts) => ({
type: DISTRICTS_DATA_SUCCESS,
payload: districts
})
// Sagas:
export function* watchGetDistricts () {
takeEvery(GET_DISTRICTS, getDistricts);
}
function* getDistricts() {
let response = yield fetch(url);
let data = yield response.json();
let list = [];
data.AdresList.Adresler.Adres.forEach((item) => {
console.info(item);
list.push({
label: item.ADI,
value: item.ID
});
});
yield put(districtsDataSuccess(list));
}
// reducer:
export default (state = initialState, action) => {
switch (action.type) {
case DISTRICTS_DATA_SUCCESS:
return {
...state,
districts: action.payload
};
default:
return state;
}
};

React is not passing state to mapStateToProps

I'm trying to pass an array of items(state) in mapStateToProps. However i get an empty array or it shows undefined.
App.js
import React, { Component } from 'react';
import PostList from './PostList';
import Axios from '../Axios';
import {connect} from 'react-redux';
import { withRouter, Redirect} from 'react-router-dom';
import {DeletePost, GetPosts} from '../actions/';
const Styles = {
myPaper:{
margin: '20px 0px',
padding:'20px'
}
,
wrapper:{
padding:'0px 60px'
}
}
class Posts extends Component {
state = {
posts: [],
loading: true,
}
getPosts = () => {
Axios.get(process.env.REACT_APP_GET_POSTS)
.then( (res) => {
this.setState({
posts: res.data,
loading: false
})
})
// console.log(this.state.posts);
}
componentWillMount(){
this.getPosts();
}
componentDidMount(){
// doesn't show posts in here
console.log(this.props.posts)
this.props.GetPosts(this.state.posts);
}
onDelete = (id) => {
Axios.post(`/api/posts/delete/${id}`);
this.setState({
posts: this.state.posts.filter(post => post.id !== id)
})
}
render() {
const {loading, posts} = this.state;
if (!this.props.isAuthenticated) {
return (<Redirect to='/signIn' />);
}
if(loading){
return "loading..."
}
return (
<div className="App" style={Styles.wrapper}>
<h1> Posts </h1>
<PostList DeletePost={this.onDelete} posts={posts}/>
</div>
);
}
}
const mapStateToProps = (state) => ({
isAuthenticated: state.user.isAuthenticated,
posts: state.user.posts
})
const mapDispatchToProps = (dispatch, state) => ({
// newPost: (post) => dispatch(newPost(post)),
// DeletePost: (id) => dispatch( DeletePost(id))
GetPosts: (posts) => dispatch( GetPosts(posts))
});
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Posts));
Reducer.js
import { GET_POSTS} from '../actions/';
const initialState = {
post: [],
postError: null,
posts:[]
}
export default (state = initialState, action) => {
switch (action.type) {
// doesn't get posts
case GET_POSTS:
return({
...state,
posts: action.posts
})
default:
return state
}
actions
export const GetPosts = (posts) => {
return (dispatch, getState) => {
dispatch({type: GET_POSTS, posts })
console.log('this works i guess', posts);
}
}
I would advice you not to save posts in two places. That somewhat defeats the purpose of using redux. You actually don't need post as a state variable in Posts class. Whenever there is a new state in redux store associated Class will fall into updation cycle.
Also, you can have a look at redux-thunk if you are making api calls.
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
It will help you move api fetching logic to actions and reducers and thus rendering your views clean.
Change this
export const GetPosts = (posts) => {
return (dispatch, getState) => {
dispatch({type: GET_POSTS, posts })
console.log('this works i guess', posts);
}
}
to
export const GetPosts = (posts) => {
return (dispatch, getState) => {
Axios.get(process.env.REACT_APP_GET_POSTS)
.then( (res) => {
dispatch({type: GET_POSTS, res.data })
})
})
}
}
Change this
componentWillMount(){
this.getPosts();
}
to
componentWillMount(){
this.props.GetPosts();
}
Now you wont be needing a componentDidUpdate.
Also, if you are wondering how to show Loading... till the api call is not completed, you can add a key isFetching to your store.
const initialState = {
post: [],
postError: null,
posts:[],
isFecthing: false
}
and can add an action something like ChangeFetchStats
export const GetPosts = (posts) => {
return (dispatch, getState) => {
dispatch({type: CHANGE_STATE, false});
Axios.get(process.env.REACT_APP_GET_POSTS)
.then( (res) => {
dispatch({type: CHANGE_STATUS, true);
dispatch({type: GET_POSTS, res.data })
})
})
}
}
Sometimes, it may take time over a network to get a POST response. In such case, if ur component gets mounted, it will make a call to the action, but because it takes time, you will get response empty/ undifined posts array.
To prevent this from happening, you can go with following :
componentDidMount(){
this.props.GetPosts(this.state.posts);
if(!this.props.posts){
console.log(this.props.posts);
}
}
A little tweek in the render method may help too:
render() {
const {loading, posts} = this.props;
if (!this.props.isAuthenticated) {
return (<Redirect to='/signIn' />);
}
if(loading){
return "loading..."
}
return (
{ posts &&(
<div className="App" style={Styles.wrapper}>
<h1> Posts </h1>
<PostList DeletePost={this.onDelete} posts={posts}/>
</div>
);)}
}
So i finally found a purpose for componentDidupdate
The app took a little bit long to load posts maybe half a second.
So by calling componentDidUpdate, i get the posts after its finished rendering.
componentDidUpdate(){
this.props.GetPosts(this.state.posts);
}
Along with another solution by #stevek
change this
case GET_POSTS:
return({
...state,
posts: state.posts
})
to this
import { GET_POSTS} from '../actions/';
const initialState = {
post: [],
postError: null,
posts:[]
}
export default (state = initialState, action) => {
switch (action.type) {
// doesn't get posts
case GET_POSTS:
return{...state, posts: action.posts}
default:
return state
}
}
And i can see it after its rendered

How can i access certain res.data from async action?

I am trying to access data received from web API to action in component. I set up registerUser action that posts new user data to API and then it is being sent to DB. API sents back status in JSON format. I want to render errors/notifications based on what was being passed as value of status key.
EDIT: I added key status in redux state, in REGISTER_USER type of action i am assigning value to it according to status being sent from backend.
However, i cannot access this propery in state by this.props.state/this.props.user - console loging it results in "undefined"
authActions.js
const authState = {
users: [],
status: ''
}
export const registerUser = user => dispatch => {
axios.post('https://damianlibrary.herokuapp.com/users/register', user)
.then(res => dispatch({
type: REGISTER_USER,
payload: res.data,
status: res.data.status
}))
}
authReducer.js
import { LOGIN_USER, REGISTER_USER } from '../actions/types';
const authState = {
users: []
}
export default function(state = authState, action) {
switch(action.type) {
case LOGIN_USER:
return {
...state
};
case REGISTER_USER:
return {
...state,
users: [action.payload, ...state.users]
};
default:
return state;
}
}
RegistrationForm.js component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { registerUser } from '../../actions/authActions';
import './RegisterForm.css';
class RegisterForm extends Component {
state = {
user_name: '',
password: '',
}
onChangeHandler = (e) => {
this.setState({ [e.target.name]: e.target.value })
};
onSubmitHandler = (e) => {
const { user_name, password } = this.state
const newUser = {
user_name: user_name,
password: password
}
this.props.registerUser(newUser)
this.setState({
user_name: '',
password: ''
})
e.preventDefault();
}
render() {
const { user_name, password } = this.state;
return (
<div className='formContainer'>
<div className='form'>
<form className='bookForm' onSubmit={this.onSubmitHandler.bind(this)}>
<div className='inputs'>
<input
type='text'
name='user_name'
placeholder='Username'
onChange={this.onChangeHandler}
value={user_name}/>
<input
type='password'
name='password'
placeholder='Password'
onChange={this.onChangeHandler}
value={password}/>
</div>
<div className='buttonSpace'>
<button>Register</button>
</div>
</form>
</div>
</div>
)
}
}
const mapStateToProps = (state) => ({
user: state.user
});
export default connect(mapStateToProps, { registerUser })(RegisterForm);
Do i have to get such value in my App container (It is in ), then get status: state.status (redux state) and pass it via props to my RegisterForm component?
store.js
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [thunk];
const store = createStore(rootReducer, initialState, compose(
applyMiddleware(...middleware)
));
export default store;
rootReducer.js
import { combineReducers } from 'redux';
import bookReducer from './bookReducer';
import authReducer from './authReducer';
export default combineReducers({
book: bookReducer,
auth: authReducer
});
Fixed my issue. I called auth: authReducer in my rootReducer.js file and after that i tried to get what my reducer was returning by calling user: state.user instead of user: state.auth.
I can reach my redux state without any problems now.

Component isn't updating after state change

I have read through 100's of these threads on here, and I can't seem to understand why my component isn't updating. I am pretty sure it has something to do with the Immutability, but I can't figure it out.
The call is being made, and is returning from the server. The state is changing (based on the redux-Dev-Tools that I have installed).I have made sure to not mutate the state in any instance, but the symptoms seem to point that direction.
Code Sandbox of whole app https://codesandbox.io/s/rl7n2pmpj4
Here is the component.
class RetailLocationSelector extends Component {
componentWillMount() {
this.getData();
}
getData = () => {
this.props.getRetailLocations()
}
render() {
const {data, loading} = this.props;
return (
<div>
{loading
? <LinearProgress/>
: null}
<DefaultSelector
options={data}
placeholder="Retail Location"/>
</div>
);
}
}
function mapStateToProps(state) {
return {
loading: state.retaillocations.loading,
data: state.retaillocations.data,
osv: state.osv};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
getRetailLocations,
selectRetailLocation,
nextStep
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(RetailLocationSelector);
And here is my reducer :
import {REQUEST_RETAIL_LOCATIONS, SUCCESS_RETAIL_LOCATIONS,
ERR_RETAIL_LOCATIONS, SELECT_RETAIL_LOCATION} from
'../actions/RetailLocationsAction'
const initialState = {
data: [],
loading: false,
success: true,
selectedRetailLocation: undefined
}
function retailLocation(state = initialState, action) {
switch (action.type) {
case REQUEST_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: true
}, {success: true})
case SUCCESS_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: false
}, {
success: true
}, {
data: Object.assign([], action.payload.data)
})
case ERR_RETAIL_LOCATIONS:
return Object.assign({}, state, {
loading: false
}, {
success: false
}, {errorMsg: action.payload.message})
case SELECT_RETAIL_LOCATION:
return Object.assign({}, state, {
selectedRetailLocation: state
.data
.find((rec) => {
return rec.id === action.payload.id
})
})
default:
return state;
}
}
export default retailLocation
And finally, my Action file:
import axios from 'axios';
//import {api} from './APIURL'
export const REQUEST_RETAIL_LOCATIONS = 'REQUEST_RETAIL_LOCATIONS'
export const SUCCESS_RETAIL_LOCATIONS = 'SUCCESS_RETAIL_LOCATIONS'
export const ERR_RETAIL_LOCATIONS = 'ERR_RETAIL_LOCATIONS'
export const SELECT_RETAIL_LOCATION = 'SELECT_RETAIL_LOCATION'
const URL = 'localhost/api/v1/retail/locations?BusStatus=O&LocType=C'
export const getRetailLocations = () => (dispatch) => {
dispatch({ type: 'REQUEST_RETAIL_LOCATIONS' });
return axios.get(URL)
.then(data => dispatch({ type: 'SUCCESS_RETAIL_LOCATIONS', payload: data }))
.catch(error => dispatch({type : 'ERR_RETAIL_LOCATIONS', payload: error}));
}
Combined Reducer
import { combineReducers } from "redux";
import retailLocations from './RetailLocationsReducer'
import vendors from './VendorsReducer'
import receiptInformation from './ReceiptInfoReducer'
import osv from './OSVReducer'
import receiptDetail from './ReceiptDetailReducer'
const allReducers = combineReducers({
retaillocations: retailLocations,
vendors: vendors,
receiptInformation: receiptInformation,
receiptDetail: receiptDetail,
osv: osv
});
export default allReducers;
This answer doesn't solve your issue totally but provides some hints about what is not working. The broken part is your store definition. I don't have much experience with redux-devtools-extension or redux-batched-subscribe but if you define your store like that your thunk function works:
const store = createStore(reducer, applyMiddleware(thunk));
One of the configuration for the mentioned packages above brokes your thunk middleware.

Resources