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.
Related
I am studying redux-saga and I want to fetch data from :
https://jsonplaceholder.typicode.com/posts
and in my redux folder I have the fallowing:
(it can be checked in this github repository
https://github.com/jotasenator/redux-saga-fetching-example/tree/main/src)
\src\redux\api.js
import axios from 'axios'
export const loadPostApi = async () => {
await axios.get(`https://jsonplaceholder.typicode.com/posts`)
}
the get request to the address in question
src\redux\app.actions.js
export const loadPostStart = () => ({
type: 'LOAD_POST_START',
})
export const loadPostSuccess = (posts) => ({
type: 'LOAD_POST_SUCCESS',
payload: posts,
})
export const loadPostFail = (error) => ({
type: 'LOAD_POST_FAIL',
payload: error,
})
those are the actions functions
src\redux\app.reducer.js
const INITIAL_STATE = {
loading: false,
posts: [],
errors: null,
}
export const appReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case 'LOAD_POST_START':
return {
...state,
loading: true,
}
case 'LOAD_POST_SUCCESS':
return {
...state,
posts: action.payload,
loading: false,
}
case 'LOAD_POST_FAIL':
return {
...state,
errors: action.payload,
loading: false,
}
default:
return state;
}
}
the reducer of the fetching, updating state,
src\redux\counterReducer.js
import { types } from "./types";
const initialState = {
value: 0
}
export const counterReducer = (state = initialState, action) => {
switch (action.type) {
case types.adicionar:
return {
...state,
value: state.value + 1
}
case types.resetear:
return {
...state,
value: 0
}
case types.restar:
return {
...state,
value: state.value - 1
}
default:
return state
}
}
this is the reducer of the counter app, with different approach, types are isolated in another file
src\redux\rootReducer.js
import { combineReducers } from 'redux'
import { counterReducer } from './counterReducer'
import { appReducer } from './app.reducer'
export const rootReducer = combineReducers({
counterReducer,
appReducer
})
the rootReducer for gathering the reducers
src\redux\sagas.js
import { put, takeLatest, call } from 'redux-saga/effects'
import { loadPostApi } from './api'
import { loadPostFail, loadPostSuccess } from './app.actions'
export function* onLoadPostStartAsync() {
try {
const response = yield call(loadPostApi)
yield put(loadPostSuccess(response.data))
} catch (error) {
yield put(loadPostFail(error))
}
}
export function* onLoadPost() {
yield takeLatest('LOAD_POST_START', onLoadPostStartAsync)
}
export default function* rootSaga() {
yield ([
onLoadPost(),
])
}
saga onLoadPostStartAsync called by saga onLoadPost inside rootSaga
src\redux\store.js
import { applyMiddleware, compose, createStore } from "redux";
import createSagaMiddleware from 'redux-saga'
import { rootReducer } from "./rootReducer";
import rootSaga from "./sagas";
const sagaMiddleware = createSagaMiddleware()
const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
const enhancer = composeEnhancers(applyMiddleware(sagaMiddleware))
export const store = createStore(rootReducer, enhancer)
sagaMiddleware.run(rootSaga)
this is the store with the redux_devtool_extension, the reducers, and running rootSaga
src\redux\types.js
export const types = {
adicionar: 'ADICIONAR',
resetear: 'RESETEAR',
restar: 'RESTAR'
}
those are the types of the counterApp reducer
src\Counter.js
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
export const Counter = () => {
const dispatch = useDispatch()
const { value } = useSelector(state => state.counterReducer)
const handleAdicionar = () => {
dispatch({ type: 'ADICIONAR' })
}
const handleResetear = () => {
(value !== 0) && dispatch({ type: 'RESETEAR' })
}
const handleRestar = () => {
dispatch({ type: 'RESTAR' })
}
console.log(value)
return (
<div>
<button onClick={handleAdicionar}>Adicionar</button>
{' '}
<button onClick={handleResetear}>Resetear</button>
{' '}
<button onClick={handleRestar}>Restar</button>
<hr />
</div>
)
}
this is the Counter component, it works ok
src\Fetching.js
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { loadPostStart } from './redux/app.actions'
export const Fetching = () => {
const dispatch = useDispatch()
const fetchPost = () => {
dispatch(loadPostStart())
}
const state = useSelector(state => state.appReducer)
console.log(state)
return (
<>
<h1>Fetching from https://jsonplaceholder.typicode.com</h1>
<button onClick={fetchPost}>Fetching</button>
{
!state.loading && state.posts.map((post) => (
<li key={post.id}><h2>{post.title}</h2></li>
))
}
</>
)
}
the Fetching component click on the button calls fetchPost function who dispatch loadPostStart() function which is the same of dispatching {type: 'LOAD_POST_START'}, but nothing happens here when clicking, not fetch nothing from here https://jsonplaceholder.typicode.com/posts
src\index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { store } from './redux/store';
import { Provider } from "react-redux";
import { Unificator } from './Unificator';
ReactDOM.render(
<Provider store={store}>
<Unificator />
</Provider>,
document.getElementById('root')
);
component Unificator has Counter and Fetching component
src\Unificator.js
import React from 'react'
import { Counter } from './Counter'
import { Fetching } from './Fetching'
export const Unificator = () => {
return (
<div>
<Counter />
<Fetching />
</div>
)
}
as you can see is about of two reducers, one is the famous counter, and the another one is the fetching issue, do not know what is happening that is not fetching the data
obviously, i am doing something wrong here...don t see where
Axio returns promise, You need to capture that and return. Please try replacing below code.
export const loadPostApi = async () => {
await axios.get(`https://jsonplaceholder.typicode.com/posts`)
.then((response) => {
console.log('Response', response);
return response;
})
.catch((error) => {
console.log('error', error);
})
}
Help, I can not display data from the server. In the console I see that they come, but as soon as I try to access the array or call it via map, the result is undefined. Maybe where is the problem with the implementation? Newbie in redux, if possible I ask you to answer in more detail.
App.js
import React from 'react';
import {Component} from "react";
import './App.css';
import {connect} from 'react-redux';
import {fetchUsersListRequest} from "./action/users";
class App extends Component {
componentDidMount() {
const {usersData} = this.props;
usersData("https://reqres.in/api/users?page=2");
}
render() {
const {users} = this.props;
return (
<div className="apiList">
<ul>
{users.map(userItem =>
<li>{userItem}</li>
)}
</ul>
</div>
);
}
}
function mapStateToProps(state) {
return {
states: state.cars,
users: state.users
}
}
function mapDispatchToProps(dispatch) {
return {
addCar: (newCar) => {
dispatch({type: 'NEW_CAR', payload: newCar})
},
usersData: (url) => {
dispatch(fetchUsersListRequest(url))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
store.js
import {combineReducers, createStore, applyMiddleware} from 'redux';
import thunk from "redux-thunk";
import reducers from './reducers/reducers';
import usersList from './reducers/usersList';
const allReducers = combineReducers({
cars : reducers,
users: usersList
})
let store = createStore(allReducers,
applyMiddleware(thunk));
export default store;
reducer.js
const initialState = [];
export default function usersList(state = initialState, action) {
switch(action.type) {
case "USERS_LIST_DATA":
return action.users
default:
return state;
}
action.js
export function fetchUsersList(users) {
return {
type: "USERS_LIST_DATA",
users
}
}
export function fetchUsersListRequest(url) {
return(dispatch)=> {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response;
}
)
.then(response => response.json())
.then(response => dispatch(fetchUsersList(response)))
}
}
That's because your users are not directly contained in response. So, you need to dispatch your users like this:
export function fetchUsersListRequest(url) {
return(dispatch)=> {
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
return response;
}
)
.then(response => response.json())
.then(response => dispatch(fetchUsersList(response.data)))
}
}
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`
});
so I'm using React-Router to make a simple blog page. The DELETE request works fine, however it redirects to the index page before the post has been deleted, causing an error if you click on said post (id is not defined). I have set up a call back on the action creator to ensure the request finishes before redirecting, as well as on the deletePost function that is being called. However it is still redirecting before the request finishes deleting, thanks for taking a look!
posts_show.js with onDeleteClick function
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import { fetchPost } from '../actions';
import { deletePost } from '../actions';
class PostsShow extends Component {
componentDidMount() {
const { id } = this.props.match.params;
this.props.fetchPost(id);
}
onDeleteClick() {
const { id } = this.props.match.params;
this.props.deletePost(id, () => {
this.props.history.push('/');
});
}
render() {
const { post } = this.props;
if(!post) {
return <div> Loading.. </div>
}
return <div className="container">
<h3>{post.title}</h3>
<h6>Categories: {post.categories}</h6>
<p>{post.content}</p>
<Link to="/" className="btn btn-secondary">
Back
</Link>
<Link to="/" className="btn btn-danger btn-sm ml-2"
onClick={this.onDeleteClick.bind(this)}
>Delete</Link>
</div>;
}
}
function mapStateToProps({ posts }, ownProps) {
return { post: posts[ownProps.match.params.id] };
}
export default connect(mapStateToProps, { fetchPost, deletePost })(PostsShow);
actions/index.js with deletePost request
import axios from 'axios';
export const FETCH_POSTS = 'fetch_posts';
export const CREATE_POST = 'create_post';
export const FETCH_POST = 'fetch_post';
export const DELETE_POST = 'delete_post';
const ROOT_URL = 'http://reduxblog.herokuapp.com/api';
const API_KEY = '?key=dmitriiiii88';
export function fetchPosts() {
const request = axios.get(`${ROOT_URL}/posts${API_KEY}`)
return {
type: FETCH_POSTS,
payload: request
}
}
export function createPost(values, callback) {
const request = axios.post(`${ROOT_URL}/posts${API_KEY}`, values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
}
}
export function fetchPost(id) {
const request = axios.get(`${ROOT_URL}/posts/${id}${API_KEY}`)
return {
type: FETCH_POST,
payload: request
}
}
export function deletePost(id, callback) {
const request = axios.delete(`${ROOT_URL}/posts/${id}${API_KEY}`)
.then(() => callback());
return {
type: DELETE_POST,
payload: id
}
}
Your action is returning the type / payload object before your API call finishes.
If you aren't already, you need to use some sort of redux middleware to allow you to return a function.
redux-thunk or redux-saga are both good options
You need to create your store and pass in the middleware like so:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
export default(initialState) => {
return createStore(rootReducer, initialState, applyMiddleware(thunk));
}
Then in you actions, you can return a function which dispatches your action to your reducer:
export function deletePost(id, callback) {
return (dispatch) => {
axios.delete(`${ROOT_URL}/posts/${id}${API_KEY}`)
.then(() => {
return {
type: DELETE_POST,
payload: id
}
}.catch(err => console.log(`err: ${err}`))
}
}
Notice you don't need to assign your axios call to a variable either.
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});
}