redux-promise use 'then()' after dispatching action - reactjs

I want to delete a post. I'm using redux-promise as middleware.
My action looks like this:
export function deletePost(id) {
const request = axios.delete(`${ROOT_URL}/${id}?apiKey=${API_KEY}`)
return {
type: DELETE_POST,
payload: request
}
}
Then I have a button in my component to trigger action.
onDeleteClick() {
deletePost(id)
.then(setState({ redirect: true }))
}
The problem is that I can't use 'then()'. I would simply like to redirect user to homepage after deleting post.
Please help me guys.
Source code on request.
actions/index.js
import axios from 'axios';
export const DELETE_POST = 'DELETE_POST';
export const ROOT_URL = 'example.com';
export const API_KEY = 'randomstring';
export function deletePost(id) {
const request = axios.delete(`${ROOT_URL}/${id}?apiKey=${API_KEY}`)
return {
type: DELETE_POST,
payload: request
}
}
reducers/post_reducer.js
import { DELETE_POST } from '../actions/index';
const INITIAL_STATE = { all: [], post: null };
export default function(state = INITIAL_STATE, action) {
switch(action.type) {
return state.all.filter(post => post !== action.payload.data);
default:
return state;
}
}
components/PostShow.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchPost, deletePost } from '../actions/index';
import { Link } from 'react-router-dom';
import { Redirect } from 'react-router';
class PostsShow extends Component {
constructor(props) {
super(props)
this.state = {
redirect: false
}
}
componentWillMount() {
this.props.fetchPost(this.props.match.params.id)
}
onDeleteClick() {
deletePost(this.props.match.params.id)
.then(() => this.setState({ redirect: true }))
}
render() {
if(!this.props.post) {
return <div>Loading...</div>
}
if(this.state.redirect) {
return <Redirect to='/'/>
}
return (
<div className='blog-post container'>
<h3>{this.props.post.title}</h3>
<h6>Categories: {this.props.post.categories}</h6>
<p>{this.props.post.content}</p>
<Link to='/'><button className='btn btn-primary'>Back</button></Link>
{ this.props.user
? <button className='btn btn-danger' onClick={this.onDeleteClick.bind(this)}>Delete</button>
: null }
</div>
);
}
}
function mapStateToProps(state) {
return {
post: state.posts.post,
user: state.user
}
}
export default connect(mapStateToProps, { fetchPost, deletePost }) (PostsShow)

Per the redux-promise source code, it looks like it should return the chained promise:
return isPromise(action.payload)
? action.payload.then(
result => dispatch({ ...action, payload: result }),
error => {
dispatch({ ...action, payload: error, error: true });
return Promise.reject(error);
}
)
: next(action);
So, I would assume that you could chain off of the dispatch.
That said, your snippet has a couple potential issues. If that's in a component, then where is deletePost coming from? You're also not using this in front of setState. Assuming that deletePost is a bound-up action creator, this example should be correct:
onDeleteClick() {
this.props.deletePost(id)
.then(() => this.setState({redirect : true});
}

Related

Infinite loop when fetch data in middleware

I try to fetch data and display it into a react component, but i have an infinite loop on the fetch call in my middleware and action seems not dispatched. i Receive no result in my post component.
Action.js :
import { DATA_LOADED } from './../constants/action-types';
export function getData() {
return {type: DATA_LOADED}
}
Middleware :
export function asyncMiddleWare({dispatch}) {
return function(next) {
return function (action) {
if (action.type === DATA_LOADED) {
return fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(json => {
console.log('---');
console.log('infinite calls');
console.log('---');
dispatch({type:DATA_LOADED, payload: json});
})
}
return next(action);
}
}
}
Reducer :
if (action.type === DATA_LOADED) {
return Object.assign({}, state, {
articles: state.remoteArticles.concat(action.payload)
})
}
and the store
import {createStore, applyMiddleware, compose} from 'redux';
import rootReducer from '../reducers/index';
import {asyncMiddleWare } from "../middleware";
import thunk from "redux-thunk";
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, storeEnhancers(applyMiddleware(asyncMiddleWare, thunk)));
export default store;
I load data in componentDidMount method in my component :
import React from "react";
import { connect } from "react-redux";
import { getData } from "./js/actions/index";
class Post extends React.Component {
componentDidMount() {
this.props.getData();
}
render () {
console.log(this.props.articles);
return (
<div className='post'>
{this.props.articles.map(article => (
<div className='post'>
{article}
</div>
))}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
articles: state.remoteArticles.slice(0, 10)
};
}
export default connect(
mapStateToProps,
{getData}
)(Post);
If you look into your middleware resolved promise function you'll notice that you are dispatching action of same type (DATA_LOADED) again which causes middleware to process it again.
Take a look at this approach
export function asyncMiddleWare({dispatch}) {
return function(next) {
return function (action) {
if (action.type === DATA_LOAD_REQUEST) {
return fetch('https://jsonplaceholder.typicode.com/posts')
.then(response => response.json())
.then(json => {
console.log('---');
console.log('infinite calls');
console.log('---');
dispatch({type:DATA_LOAD_SUCCESS, payload: json});
}, (error) => {
dispatch({type:DATA_LOAD_ERROR, payload: error});
})
}
return next(action);
}
}
}
You should separate your REQUEST, SUCCESS and ERROR calls so when you call each of those actions you don't end up in infinite loop.

Redux doesn't fetch data from API request

I'm new to React/Redux. I'm making an app using an API but the code doesn't work. When I run the code it says "this.props.recipes.map is not a function" and doesn't render anything.
If I change payload to: "payload: response.data.recipes" then the error changes to "Given action "FETCH_RECIPE", reducer "recipes" returned undefined." but no errors on screen (only in console). I thought writing "(state = [], action)" would solve the problem but it seems not. What's the problem and how do I fix this error?
Action Creator
import recipe from '../apis/recipe';
export const fetchRecipe = () => async dispatch => {
const response = await recipe.get('');
dispatch({ type: 'FETCH_RECIPE', payload: response.data })
};
Reducer
import { combineReducers } from 'redux';
const recipeReducer = (state = [], action) => {
switch(action.type) {
case 'FETCH_RECIPE':
return action.payload;
default:
return state;
}
};
export default combineReducers({
recipes: recipeReducer
});
import React from 'react';
import { connect } from 'react-redux';
import { fetchRecipe } from '../actions';
class Recipe extends React.Component {
componentDidMount() {
this.props.fetchRecipe();
console.log("This doesn't work", this.props.recipes)
}
renderList() {
return this.props.recipes.map(recipe => {
return (
<div>
<p>{recipe.publisher}</p>
</div>
)
})
}
render() {
console.log("First loaded: empty, second time: data fetched", this.props.recipes)
return (
<div>
{this.renderList()}
</div>
);
}
}
const mapStateToProps = (state) => {
return { recipes: state.recipes }
};
export default connect(mapStateToProps,{
fetchRecipe
})(Recipe);
API Request
import axios from 'axios';
import { key } from './config';
export default axios.create({
baseURL: `https://cors-anywhere.herokuapp.com/https://www.food2fork.com/api/search?key=${key}&q=pizza`
});

Dispatch not changing redux state

I am fairly new to redux, and I am running into a problem.
I am trying to implement flash messages to my login page, but redux's dispatch is not changing the UI State.
I want a flash message to appear on the login page after user successfully register.
//login.js
class Login extends Component{
renderMessage() {
if (this.props.flashMessageType== "registrationComplete"){
return (
<Message
style={{textAlign: "left"}}
success
icon="check circle"
header="Account Registration was Successful"
list={["You must verify your email before logging in"]}
/>
);
} else {
return (null);
}
}
render() {
return ({
this.renderMessage()
});
}
}
function mapStateToProps(state) {
return {
flashMessageType:state.flashMessage.flashType,
};
}
export default connect(mapStateToProps, actions)(Login);
Here is the reducer
const initialState = {
flashType: "",
};
export default function(state = {initialState}, action){
switch(action.type){
case USER_REGISTER:
return [
...state,
{
flashType:"registrationComplete"
}
];
default:
return initialState;
}
}
and here is the actions
export const submitForm = (values,history) => async dispatch => {
const res = await axios.post('/api/signup', values);
history.push('/');
dispatch({type: FETCH_USER, payload: res.data});
dispatch({type: USER_REGISTER});
};
I appreciate your help.
Thanks,
Vincent
As Amr Aly mentioned (and now soroush), you're essentially mutating the state when you do:
return[ ...state, { flashType:"registrationComplete" }]
What you really want is:
return { ...state, flashMessage: "registrationComplete" }
Also, some of your code is a bit redundant and/or missing some important instructions (like try/catch blocks).
What your code should look like:
FlashMessage.js
import React, { PureComponent } from 'react';
import Message from '../some/other/directory';
import actions from '../some/oter/directory':
class Login extends PureComponent {
render = () => (
this.props.flashMessage == "registrationComplete"
? <Message
style={{textAlign: "left"}}
success
icon="check circle"
header="Account Registration was Successful"
list={["You must verify your email before logging in"]}
/>
: null
)
}
export default connect(state => ({ flashMessage: state.auth.flashMessage }), actions)(Login)
reducers.js
import { routerReducer as routing } from 'react-router-redux';
import { combineReducers } from 'redux';
import { FETCH_USER, USER_REGISTER } from '../actions/types';
const authReducer = (state={}, ({ type, payload }) => {
switch(type){
case FETCH_USER: return { ...state, loggedinUser: payload };
case USER_REGISTER: return { ...state, flashMessage: "registrationComplete" }
default: return state;
}
}
export default = combineReducers({
auth: authReducer,
routing
});
actions.js
import { FETCH_USER, USER_REGISTER } from './types';
export const submitForm = (values,history) => async dispatch => {
try {
const {data} = await axios.post('/api/signup',values);
dispatch({ type:FETCH_USER, payload: data });
dispatch({ type:USER_REGISTER });
history.push('/');
catch (err) {
console.error("Error: ", err.toString());
}
};
Your reducer should be:
const initialState = {
flashType: "",
};
export default function(state = initialState, action){
switch(action.type){
case USER_REGISTER:
return {
...state,
flashType: "registrationComplete",
};
default:
return state;
}
}

Best way to pass value from component to other component's redux-saga

I have a question about passing value (item.id) from one component to another component's saga, where I could add additional field in POST body and make a request.
I have two components: 1st Form component, where is two input fields. 2st component is Item, which are GET'ed from API. So there is a itemId value, which I need to give when making POST request with form.
My soliution right now is to pass itemId to localstorage and then take it in saga, but it causes some bugs when user opens two browser windows. What would be better solution for this task?
My Item component:
export class FindClientItem extends React.PureComponent {
constructor() {
super();
this.state = {
modalIsOpen: false,
};
this.openModal = this.openModal.bind(this);
this.closeModal = this.closeModal.bind(this);
}
openModal() {
this.setState({ modalIsOpen: true });
}
closeModal() {
this.setState({ modalIsOpen: false });
localStorage.removeItem('itemId');
}
render() {
const { item } = this.props;
if(this.state.modalIsOpen){
localStorage.setItem('itemId',item.itemId);
}
// Put together the content of the repository
const content = (
<Wrapper>
<h3>{item.title}</h3>
Details: {item.description}...<button onClick={this.openModal}>
More
</button>
<Modal
isOpen={this.state.modalIsOpen}
onRequestClose={this.closeModal}
style={customStyles}
contentLabel="Modal"
>
<h3>{item.title}</h3>
Details: {item.description} <br />
<button onClick={this.openBidModal}>Submit</button>{' '}
</Modal>
</Wrapper>
);
// Render the content into a list item
return <ListItem key={`items-${item.itemId}`} item={content} />;
}
}
And then my other 1st Form component's saga:
export function* submitForm() {
try {
const formType = 'item';
const body = yield select(makeSelectModifiedData());
body.itemId = localStorage.getItem('itemId');
let requestURL;
switch (formType) {
case 'item':
requestURL = 'http://localhost:1234/item';
break;
default:
}
const response = yield call(request, requestURL, { method: 'POST', body });
} catch (error) {
Alert.error('Error message...', {
html: false,
});
}
}
Not sure if this is the "Best" way to do this, however, works well for me. Have you tried creating a shared js file (imported into both components) which GETS / SETS a variable? for example.
shared.js
let data = null;
setData(d){
data = d;
}
getData(){
return data;
}
addChangeListner(eventName, callback){
this.on(eventname, callback);
}
dispatcherCallback(action){
switch(action.actionType){
case 'SET_DATA':
this.getData();
}
}
Whenever you require your component to update, you can add an change listener to then return the new data once set so the components aren't out of sync. Just remember to remove the listener afterwords!
Component
componentDidMount(){
shared.addChangeListner('SET_DATA', this.onUpdate)
}
// use x to pass to your saga...
onUpdate(){
var x = shared.getData();
}
Hope this helps!
index.js
import {handleSave, loadData } from './action';
import Modal from './Modal',
export class GetFormData extends React.PureComponent {
componentDidMount() {
this.props.loadData();
}
saveData = (data) => {
this.props.handleSave(data)
}
render() {
return (
<div>
<Modal
isOpen={this.state.modalIsOpen}
onRequestClose={this.closeModal}
style={customStyles}
contentLabel="Modal"
data={this.props.getdata}
handlePost={this.saveData}
/>
</div>
)
}
}
const mapStateToProps = state => ({
getdata: state.formData,
});
const mapDispatchToProps = dispatch => ({
loadData: bindActionCreators(loadData, dispatch),
handleSave: bindActionCreators(handleSave, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(GetFormData);
actions.js
import {
LOAD_DATA,
LOAD_DATA_SUCCESS,
LOAD_DATA_FAILED
HANDLE_SAVE,
HANDLE_SAVE_SUCCESS,
HANDLE_SAVE_FAILED
} from './constants';
export function loadData() {
return {
type: LOAD_DATA,
};
}
export function loadDataSuccess(formData) {
return {
type: LOAD_DATA_SUCCESS,
formData
};
}
export function loadDataFailed(error) {
return {
type: LOAD_DATA_FAILED,
error
};
}
export function handleSave(data) {
return {
type: HANDLE_SAVE,
data
};
}
export function handleSaveSuccess() {
return {
type: HANDLE_SAVE_SUCCESS
};
}
export function handleSaveFailed(error) {
return {
type: HANDLE_SAVE_FAILED,
error
};
}
reducers.js
import { fromJS } from 'immutable';
import {
LOAD_DATA, LOAD_DATA_SUCCESS, LOAD_DATA_FAILED,
HANDLE_SAVE,
HANDLE_SAVE_SUCCESS,
HANDLE_SAVE_FAILED
} from './constants';
const initialState = fromJS({
formData: undefined,
});
function formDataReducer(state = initialState, action) {
switch (action.type) {
case LOAD_DATA:
return state;
case LOAD_DATA_SUCCESS:
return state.set('formData', action.formData);
case LOAD_DATA_FAILED:
return state.set('errormsg', fromJS(action.errormsg));
case HANDLE_SAVE:
return state.set('data', action.data);
case HANDLE_SAVE_SUCCESS:
return state.set('message', action.message);
case HANDLE_SAVE_FAILED:
return state.set('errormsg', fromJS(action.errormsg));
default:
return state;
}
}
saga.js
import { takeEvery, call, put } from 'redux-saga/effects';
import {
LOAD_DATA,
LOAD_DATA_SUCCESS,
LOAD_DATA_FAILED,
HANDLE_SAVE,
HANDLE_SAVE_SUCCESS,
HANDLE_SAVE_FAILED
} from './constants';
export function* getFormDataWorker() {
try {
const formData = yield call(api);
if (formData) {
yield put({ type: LOAD_DATA_SUCCESS, formData });
}
} catch (errormsg) {
yield put({ type: LOAD_DATA_FAILED, errormsg });
}
}
// watcher
export function* formDataWatcher() {
yield takeEvery(LOAD_DATA, getFormDataWorker);
}
export function* saveDataWorker(action) {
try {
const message = yield call(savedata, action.data);
if (message) {
yield put({ type: HANDLE_SAVE_SUCCESS, message });
}
} catch (errormsg) {
yield put({ type: HANDLE_SAVE_FAILED, errormsg });
}
}
// watcher
export function* saveDataWatcher() {
yield takeEvery(HANDLE_SAVE, saveDataWorker);
}
// All sagas to be loaded
export default [
saveDataWatcher,
formDataWatcher,
];
Modal.js
const Modal = ({data, handlePost}) => (
{ data ? data.map(item => (
<input type="text" value={item.id} />
)
}
<Button type="submit" onClick={handlePost}/ >
)
Hope this helps!
I would suggest the following:
Remove the usage of localstorage
On componentDidUpdate dispatch an action that sets the itemId in the Redux store.
componentDidUpdate() {
this.props.setItemId({itemId: this.props.item.itemId})
}
On form submit, dispatch the same action as you are currently using to trigger the saga.
Change your makeSelectModifiedData selector to return the itemId you are storing in Redux now.

How do i properly do a GET request in react-redux?

My goal is to basically do a basic GET request in react-redux. I know how to do it with POST but not with GET because there is no event that is triggering the action.
Heres' the code for action
export function getCourses() {
return (dispatch) => {
return fetch('/courses', {
method: 'get',
headers: { 'Content-Type': 'application/json' },
}).then((response) => {
if (response.ok) {
return response.json().then((json) => {
dispatch({
type: 'GET_COURSES',
courses: json.courses
});
})
}
});
}
}
Where do i trigger this to get the data? in component?
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { getCourses } from '../actions/course';
class Course extends React.Component {
componentDidMount() {
this.props.onGetCourses();
}
allCourses() {
console.log(this.props.onGetCourses());
return this.props.courses.map((course) => {
return(
<li>{ course.name }</li>
);
});
return this.props
}
render() {
return (
<div>
<ul>
{ this.allCourses() }
</ul>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
courses: state.course.courses
}
}
const mapDispatchToProps = (dispatch) => {
return {
onGetCourses: () => dispatch(getCourses)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Course);
I tried this but it doesn't work.
Course Reducer
const initialState = {
courses: []
};
export default function course(state= initialState, action) {
switch (action.type) {
case 'GET_COURSES':
return Object.assign({}, state, {
courses: action.courses
})
default:
return state;
}
}
First, onGetCourses: () => dispatch(getCourses) should be changed to onGetCourses: () => dispatch(getCourses()) (you need to actually invoke the action creator).
When it comes to where you should call the action, it is absolutely fine to do it in componentDidMount, as you have done.
In case you did not notice, you have two return's in your allCourses().
I have similar code in my codebase, but I don't use return in front of fetch and response.json() because the function should return action object.

Resources