So I've recently started learning Redux and now I'm trying to make my first app with it, but I've stumbled upon a problem which I cannot resolve on my own. Basically I want a user to click a button (there will be authentication) and fetch all his or hers data from Firebase and display it.
Here is my index.js:
// Dependencies
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import createHistory from 'history/createBrowserHistory';
import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux';
import ReduxPromise from "redux-promise";
import ReduxThunk from 'redux-thunk';
// Reducers
import rootReducer from './reducers';
// ServiceWorker
import registerServiceWorker from './registerServiceWorker.js';
// Styles
import './styles/index.css';
// Components
import App from './containers/App.js';
const history = createHistory();
const middleware = routerMiddleware(history);
// Create store
const store = createStore(
combineReducers({
...rootReducer,
router: routerReducer
}),
applyMiddleware(ReduxThunk, middleware, ReduxPromise)
)
ReactDOM.render((
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>
), document.getElementById('root'));
registerServiceWorker();
And my main container, App.js:
import React, { Component } from 'react';
import { Route, Switch, withRouter } from 'react-router-dom'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import firebase from 'firebase';
import firebaseConfig from '../firebaseConfig.js';
// Actions
import { fetchAllMonths } from "../actions/index";
// Static components
import Nav from '../components/Nav.js';
// Routes
import CurrentMonth from '../components/CurrentMonth.js';
import AddNewMonth from '../components/AddNewMonth.js';
import Archive from '../components/Archive.js';
import Settings from '../components/Settings.js';
class App extends Component {
constructor (props) {
super(props);
this.login = this.login.bind(this);
}
componentWillMount() {
firebase.initializeApp(firebaseConfig);
firebase.auth().signInAnonymously().catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
});
}
login() {
this.props.fetchAllMonths();
}
render() {
if (this.props.data === undefined) {
return (
<button onClick={this.login}>Login</button>
)
} else if (this.props.data !== undefined) {
return (
<main className="main-container">
<Nav user="user"/>
<Switch>
<Route exact path='/' component={CurrentMonth}/>
<Route path='/aktualny' component={CurrentMonth}/>
<Route path='/nowy' component={AddNewMonth}/>
<Route path='/archiwum' component={Archive}/>
<Route path='/ustawienia' component={Settings}/>
</Switch>
</main>
);
}
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({ fetchAllMonths }, dispatch);
}
function mapStateToProps({ data }) {
return { data };
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(App))
Main action, fetchAllMonths:
// import firebase from 'firebase';
// Firebase Config
// import axios from 'axios';
export const FETCH_ALL_MONTHS = 'FETCH_ALL_MONTHS';
export function fetchAllMonths() {
/*const database = firebase.database();
const data = database.ref('/users/grhu').on('value', function(snapshot) {
return snapshot.val();
});
console.log(data) */
const data = fetch('https://my-finances-app-ef6dc.firebaseio.com/users/grhu.json')
.then(async (response) => response.json())
.then(data => {
console.log(data);
return data;
}
)
console.log(data);
return {
type: FETCH_ALL_MONTHS,
payload: data
};
};
Reducers index.js:
import { combineReducers } from "redux";
import data from "./reducer_load_from_db";
const rootReducer = combineReducers({
data: data
});
export default rootReducer;
And finally my reducer:
import { FETCH_ALL_MONTHS } from "../actions/index";
export default function(state = [], action) {
switch (action.type) {
case FETCH_ALL_MONTHS:
return [action.payload.data, ...state];
default:
return state;
}
return state;
}
So I'm sure that fetch works fine, because console.log(data) gives me a valid JSON file, but second console.log(data) with the passed const gives me a promise, which then I send as a payload to a Reducer. CreateStore also seems to work, because in the React dev console I can see a "data" prop in App container. I use redux-promise which should resolve the Promise in payload and return a JSON to the store, but data remains undefined. Also tried redux-promise-middleware, but again, no luck.
What am I missing? I've looked at that code for several hours and I cannot understand what is wrong with it.
I'll appreciate all the answers, but i really want to understand the issue, not just copy-paste a solution.
Thanks in advance!
Initial Answer
From what I'm reading in your fetchAllMonths() action creator, you're setting a property on the action it returns called payload equal to the data returned from your API call.
return {
type: FETCH_ALL_MONTHS,
payload: data
};
If you logged action.payload in your reducer like so...
switch (action.type) {
case FETCH_ALL_MONTHS:
console.log(action.payload)
return [action.payload.data, ...state];
default:
return state;
}
I believe you'd see the data returned from your API call.
So then in your reducer you would be expecting action.payload as a property of the FETCH_ALL_MONTHS action. And you'd want to use the spread operator ...action.payload.
Also, to make your logic a little easier to read, why not try using an async action to fetch data and then dispatch an action that takes in the data returned from the API call?
Hope that helps!
Updated Answer
As I thought about this and read your reply to my answer, I think you may need to use an async action to ensure your data was successfully fetched. I made a really simple CodePen example using async actions.
https://codepen.io/joehdodd/pen/rJRbZG?editors=0010
Let me know if that helps and if you get it working that way.
Related
mern stack, using a class component I call Landing I use the componentDidMount method.
on form submit axios is using the get method to return my user object. I am then dispacting my user object with an exported function to my store. Recieving this error:
Actions must be plain objects. Instead, the actual type was: 'Promise'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions.
I export default the Landing component by executing connect and passing the action I had exported followed by the execution of Landing.
The index.js file is currently utilizing redux-thunk middleware.
My goal is to update the state of user so all components immediately display the content that is changing on the form submit.
App.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { reducers } from './reducers';
import App from './components/App';
const store = createStore(reducers, {}, compose(applyMiddleware(thunk)));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);
Landing.js
import React from 'react';
import { io } from 'socket.io-client';
import _ from 'underscore';
import { connect } from 'react-redux'
import './styles.css';
import { bid } from '../../actions/auction';
import { bidPlaced, loggedUser } from '../../api/index';
import { CONNECTION_PORT } from '../../config';
class Landing extends React.Component {
constructor(props) {
super(props);
this.state = {
user: ''
}
componentDidMount() {
this.setState({user: JSON.parse(localStorage.getItem('profile))}) //sets the logged in user to state
}
handleFormSubmit = async (e) => { //This function is wrapped in tryCatch in my app
e.preventDefault()
//Storing user data in an object
const userData = {email: this.state.user.result.email, id: this.state.user.result._id}
const response = await loggedUser(userData)
//which returns a promise, which does currently hold the updated userModel
this.props.bid(response)
}
render (
<div>
<form onSubmit={handleFormSubmit}>
<input value={'all the user information'}>
<button type="submit">Click Me</button>
<form/>
)
}
export default connect(null, { bid })(Landing);
in my actions directory:
auction.js
export const bid = async (user) => ({
type: EDIT,
data: user
});
reducers
bid.js
import * as actionType from '../constants/actionTypes';
let payload = null
if (localStorage.getItem('profile')) payload = localStorage.getItem('profile')
const bidReducer = (state = { authData: payload }, action) => {
switch (action.type) {
case actionType.EDIT:
localStorage.setItem('profile', JSON.stringify({ ...action?.data }));
return { ...state, authData: action.data, loading: false, errors: null };
default:
return state;
}
};
export default bidReducer;
Just remove async in the bid
export const bid = (user) => ({
type: EDIT,
data: user
});
I'm trying to implement a simple test case for using Redux-thunks with Next JS but keep getting the error
Error: Circular structure in "getInitialProps" result of page "/".
https://err.sh/zeit/next.js/circular-structure
I have gotten this all to work once before, and am sure I'm making some obvious error.
I'd appreciate any help you could provide. I've been poking at this for an hour and I'm not seeing where I'm going wrong...
I've traced it down to the dispatch within my thunk, that is dispatch(getItemsSuccess(data)) in the following code in action-creators.js. That is, if I remove that dispatch, I don't get the error.
// action-creators.js
import {GET_ITEMS_SUCCESS} from "./action-types"
import axios from 'axios'
export const getItemsSuccess = (data) => ({ type: GET_ITEMS_SUCCESS, data });
export const getItems = () => async (dispatch,getState) => {
try {
const data = await axios.get(`https://api.themoviedb.org/3/genre/movie/list?api_key=12345xyz`)
return dispatch(getItemsSuccess(data))
} catch(e) {
console.log(`error in dispatch in action-creators: ${e}`)
}
}
My _app.js is
import React from 'react'
import {Provider} from 'react-redux'
import App, {Container} from 'next/app'
import withRedux from 'next-redux-wrapper'
import configureStore from '../redux/configure-store'
class MyApp extends App {
static async getInitialProps({Component, ctx}) {
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
return {pageProps}
}
render() {
const {Component, pageProps, store} = this.props
return (
<Container>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</Container>
)
}
}
export default withRedux(configureStore, { debug: true })(MyApp);
and my index.js is
import React, {Component} from 'react'
import {connect} from 'react-redux'
import {getItems} from "../redux/action-creators"
class Index extends Component {
static async getInitialProps({store}) {
try {
await store.dispatch(getItems())
} catch(e) {
console.log(`error in dispatch in index.js: ${e.message}`)
}
}
render() {
return <div>Sample App</div>
}
}
export default connect(state => state)(Index)
and finally I configure the store thus
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './root-reducer';
const bindMiddleware = middleware => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
function configureStore(initialState = {}) {
const store = createStore(
rootReducer,
initialState,
bindMiddleware([thunk]),
);
return store;
}
export default configureStore;
Again, any help much appreciated -- I have been going over this for some time and am not seeing the missing piece...
When you return data from axios, one has to access the data within the data, to wit, instead of
const data = await
axios.get(`https://api.themoviedb.org/3/genre/movie/list?api_key=12345xyz`)
return dispatch(getItemsSuccess(data))
I should have written
axios.get(`https://api.themoviedb.org/3/genre/movie/list?api_key=12345xyz`)
return dispatch(getItemsSuccess(data.data))
Why This Error Occurred
getInitialProps is serialised to JSON using JSON.stringify and sent to the client side for hydrating the page.
However, the result returned from getInitialProps can't be serialised when it has a circular structure.
Possible Ways to Fix It
Circular structures are not supported, so the way to fix this error is removing the circular structure from the object that is returned from getInitialProps. In your case you just need to extract appropriate data like #Cerulean explained.
When I run my app, I get following errors.
Error: Given action "LoadEntries", reducer "entries" returned
undefined. To ignore an action, you must explicitly return the
previous state. If you want this reducer to hold no value, you can
return null instead of undefined.
Following is the action file.
import axios from 'axios';
export const loadEntries = () => {
return dispatch => {
axios.get('https://jsonplaceholder.typicode.com/posts')
.then(res => {
dispatch({type: 'LoadEntries', payload: res.data});
}).catch(error => {
console.error("Error: " + error);
})
}
}
Following is the reducer.
export default (state = [], action) => {
console.log("Action: " + JSON.stringify(action) + " State: " + state);
switch (action.type) {
case 'LoadEntries':
return action.payload;
default:
return state;
}
}
When I run the app I get the following log in console for reducer.
Action: {"type":"LoadEntries"} State:
Following is the Index.js file.
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import {Provider} from 'react-redux'
import reducer from './store/reducer';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
const store = createStore(reducer, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'));
Also attached a screenshot of console.
Thanks.
You are not returning the action on both the resolve or reject of promise inside the action dispatcher.
Your dispatcher should always return an action.
I would also recommend to handle the rejection in a separate action.
I wonder how to properly load data for a stateless component using a a state value, like user id, as an argument. At the moment I try to load the data in an onEnter function but I don't seem to be able to access the state at that stage.
Is it appropriate to load the data in onEnter at all or is it expected to be done somewhere else or in a different way to be able to access the state ?
Further details about the current setup
The project is using react-router v2 and the different parts are split into separate directories, like this:
/index.js
/actions/
/components
/containers
/reducers
The index.js is creating the store that holds the state.
import React from 'react';
import ReactDOM from 'react-dom';
import {createStore, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import thunk from 'redux-thunk';
import promiseMiddleware from 'redux-promise-middleware';
import {reactWeb} from './reducers';
import AppContainer from './containers/AppContainer';
import './css/index.css';
let middlewares = [thunk, promiseMiddleware()];
let store = createStore(reactWeb, applyMiddleware(...middlewares));
ReactDOM.render(
<Provider store={store}>
<AppContainer/>
</Provider>, document.getElementById('root'));
In the AppContainer data of the current user is loaded using onEnter and the data is stored in state as user. The callback is used to wait for the response before proceeding. In the onEnterTodos I need to get the id of the current user from state to use as an argument. And I don't know how to access the state there.
import {connect} from 'react-redux';
import App from '../components/App';
import {init} from '../actions';
import {getCurrentUser} from '../actions/profile';
import {getTodos} from '../actions/todos';
const mapStateToProps = (state) => {
return {
isLoggedIn: state.loggedIn,
redirectUrl: state.redirectUrl,
};
};
const mapDispatchToProps = (dispatch) => {
return {
onEnterApp: (nextState, replace, callback) => {
// Get user data
dispatch(getCurrentUser(nextState, callback));
callback();
},
onEnterTodos: (nextState, replace) => {
// Get todos of the user - how to get the userid
dispatch(getTodos(795558));
}
};
};
const AppContainer = connect(mapStateToProps, mapDispatchToProps)(App);
export default AppContainer;
Some claim that the routes should be returned from a function that will take the store as an argument, then one can pass the store as an argument to an onEnter function. That means moving the routing setup to the index.js and the problem for me was that the dispatch function isn't available then.
Here is by the way the component associated with the container:
import React from 'react';
import StartContainer from '../containers/StartContainer';
import TodosContainer from '../containers/TodosContainer';
import {Router, Route, browserHistory, IndexRoute, IndexRedirect} from 'react-router';
const App = ({onEnterApp, onEnterTodos}) => {
let routes = (
<Route path="/" onEnter={onEnterApp}>
<IndexRedirect to="start"/>
<Route path="start" component={StartContainer}/>
<Route path="todos" onEnter={onEnterTodos} component={TodosContainer}/>
</Route>
);
return (
<div>
<Router history={browserHistory} routes={routes}/>
</div>
)
};
App.propTypes = {};
export default App;
To solve this I chained the requests. So, the state is not directly accessible in the onEnter function, but in this case I could rely on the API.
The following onEnter function was changed in the AppContainer:
onEnterUsers: (nextState, replace) => {
dispatch(getEmployees());
}
Then a two new actions were created. The first one, to get info about the current user, now just returns a promise.
export const getCurrentUserPromise = (nextState, callback) => {
let headers = {
'Accept': 'application/json'
};
return apiCall('get', config.apiEndPoint + 'login/currentLogin', null, headers);
};
Finally, to get the todos I use getCurrentUserPromise and chain the request for the todos.
export const getEmployees = () => {
let headers = {
'Accept': 'application/json'
};
return dispatch => {
const payloadUser = getCurrentUserPromise()
.then(res => {
const payload = apiCall('get', config.apiEndPoint + 'todos/find?userid=' + res.userid, null, headers)
.then(res => {
return res;
});
dispatch({type: 'GET_TODOS', payload});
return res;
});
return dispatch({type: 'GET_CURRENT_USER', payloadUser});
};
};
I'm making a request in my action - pulling from an API that needs to load in some data into my component. I have it start that request when the component will mount, but I can't seem to get Redux-Promise to work correctly because it just keeps returning:
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
in my dev tools when I try to console.log the value inside of my componentWillMount method.
Here's my code below:
Store & Router
import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import { Provider } from 'react-redux';
import { Router, hashHistory } from 'react-router';
import routes from './routes';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(promiseMiddleware)
);
render(
<Provider store={store}>
<Router history={hashHistory} routes={routes} />
</Provider>,
document.getElementById('root')
);
Action
import axios from 'axios';
export const FETCH_REVIEWS = 'FETCH_REVIEWS';
export const REQUEST_URL = 'http://www.example.com/api';
export function fetchReviews() {
const request = axios.get(REQUEST_URL);
return {
type: FETCH_REVIEWS,
payload: request
};
};
Reviews Reducer
import { FETCH_REVIEWS } from '../actions/reviewActions';
const INITIAL_STATE = {
all: []
};
export default function reviewsReducer(state = INITIAL_STATE, action) {
switch(action.type) {
case FETCH_REVIEWS:
return {
...state,
all: action.payload.data
}
default:
return state;
}
}
Root Reducer
import { combineReducers } from 'redux';
import reviewsReducer from './reviewsReducer';
const rootReducer = combineReducers({
reviews: reviewsReducer
});
export default rootReducer;
Component
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchReviews } from '../../actions/reviewActions';
class Home extends Component {
componentWillMount() {
console.log(this.props.fetchReviews());
}
render() {
return (
<div>List of Reviews will appear below:</div>
);
}
}
export default connect(null, { fetchReviews })(Home);
Any and all help is greatly appreciated. Thank you.
Redux-promise returns a proper Promise object, so you may change your code a little bit to avoid immediate execution.
class Home extends Component {
componentWillMount() {
this.props.fetchReviews().then((whatever) => { console.log('resolved')})
}
// ...
}