I'm doing my first react.js app. Because of some issues with the react & redux template project in Visual Studio 2017 I ended up with a Web API in Visual Studio 2017 and a whole different react project in Visual Studio Code (I don't know if that is relevant). I'm trying to consume my Web API but my action.payload.data is always undefined. Also I get a cross-origin error. I don't get what am I doing wrong.
src/actions/index.js
import axios from 'axios';
export const FETCH_HOME = 'fetch_home';
const R00T_URL = 'http://localhost:52988/api';
export function fetchHome() {
const request = axios.get(`${R00T_URL}/home`, { crossdomain: true });
console.log(`request: ${request}`);
return {
type: FETCH_HOME,
payload: request
};
}
src/reducers/reducer_home.js
import { FETCH_HOME } from '../actions';
export default function(state = {}, action) {
if (typeof action.payload === 'undefined') {
console.log('action undefined');
return state;
}
switch (action.type) {
case FETCH_HOME:
console.log(`action: ${action}`);
return action.payload.data;
default:
return state;
}
}
src/components/home_index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchHome } from '../actions';
class HomeIndex extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.fetchHome();
}
render() {
console.log(`props: ${this.props}`);
return (
<div>
<h1>Home Index</h1>
</div>
);
}
}
function mapStateToProps(state) {
return { props: state.props };
}
export default connect(mapStateToProps, { fetchHome })(HomeIndex);
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { BrowserRouter, Route } from 'react-router-dom';
import reducers from './reducers';
import HomeIndex from './components/home_index';
import promise from 'redux-promise';
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<BrowserRouter>
<div>
<Route path="/" component={HomeIndex} />
</div>
</BrowserRouter>
</Provider>
, document.querySelector('.container'));
Your call to axios.get() is asynchronous.
You likely want your action creator to return the action object, like this:
src/actions/index.js
...
export function fetchHome(result) {
return {
type: FETCH_HOME,
payload: result
}
}
...and then perform the async request in your component and call the action creator with the results:
src/components/home_index.js
...
componentDidMount() {
axios.get(`${R00T_URL}/home`, { crossdomain: true })
.then(result => {
console.log(`result: ${result}`);
this.props.fetchHome(result)
})
.catch(err => {
// Handle error
})
}
...
If you want to keep the async part in your action creator then look at using redux-thunk:
https://www.npmjs.com/package/redux-thunk
Related
I have just gone through some redux tutorial and started to implement
i have one API call that has to be happen from redux as soon as page loads.. its possible with ComponentDidMount ,but i need to know how redux helps in achieving this.
For easiness i had shared the code in
https://codesandbox.io/s/quirky-sunset-s95gu?fontsize=14
My index.js look like
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { createStore,applyMiddleware } from "redux";
import allReducer from "./reducers";
import { Provider } from "react-redux";
import thunk from 'redux-thunk';
import "./styles.css";
let store = createStore(
allReducer,
applyMiddleware(thunk)
);
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
My Action creater look like(action/index.js)
import UserApi from "../UserApi";
export function loadUserSuccess(user) {
return { type: "LOAD_USER_SUCCESS", user };
}
export function loadUsers() {
return function(dispatch) {
return UserApi.getAllUsers()
.then(user => {
console.log("midhun");
dispatch(loadUserSuccess(user));
})
.catch(error => {
throw error;
});
};
}
and its subsequent api caliing function look like
class UserApi {
static getAllUsers() {
return fetch("https://jsonplaceholder.typicode.com/users")
.then(response => {
console.log("response", response);
return response.json();
})
.catch(error => {
return error;
});
}
}
export default UserApi;
My Reducer look like
import initialState from "./InitialState";
export default function IsLoggedReducer(state = initialState.user, action) {
console.log(state, action);
switch (action.type) {
case "LOAD_USER_SUCCESS":
return state;
default:
return state;
}
}
and my App.js look like
import React from "react";
import { connect } from "react-redux";
import * as userActions from "./action/index";
import UserList from "./UserList";
class App extends React.Component {
render() {
return (
<div>
<h1>MYpage</h1>
<UserList user={this.props.user} />
</div>
);
}
}
function mapStateToProps(state, ownProps) {
return {
user: state.user
};
}
export default connect(mapStateToProps)(App);
I had put couple of console in action creator and its subsequent function,but its not triggering.
Any help will be much much apreciated and will be highly help for beginners
You guys can check the complete set of code
https://codesandbox.io/s/quirky-sunset-s95gu?fontsize=14
In addition to the info lavor gaved, since you use combineReducers, you need to access to state by using your reducer key.
App.js
import React from "react";
import { connect } from "react-redux";
import {loadUsers} from "./action/index";
import UserList from "./UserList";
class App extends React.Component {
componentDidMount() {
this.props.loadUsers();
}
render() {
return (
<div>
<h1>MYpage</h1>
<UserList users={this.props.users} />
</div>
);
}
}
function mapStateToProps(state, ownProps) {
return {
users: state.IsLoggedReducer
};
}
export default connect(mapStateToProps, {loadUsers})(App);
I also made some corrections in the reducer file, we need to return the new state with the given payload.
import initialState from "./InitialState";
export default function IsLoggedReducer(state = initialState.user, action) {
console.log("ap", action.payload);
switch (action.type) {
case "LOAD_USER_SUCCESS":
return [...action.payload]
default:
return state;
}
}
And action file:
import UserApi from "../UserApi";
export function loadUserSuccess(users) {
return { type: "LOAD_USER_SUCCESS", payload: users };
}
export function loadUsers() {
return function(dispatch) {
return UserApi.getAllUsers()
.then(users => {
dispatch(loadUserSuccess(users));
})
.catch(error => {
throw error;
});
};
}
You can check this codesandbox for the working app.
https://codesandbox.io/s/cranky-colden-v6r6w
You are not dispatching your action, try to do it in componentDidMount (you need to map dispatch to props first):
App.js
componentDidMount() {
this.props.loadUsers();
}
// ...
function mapStateToProps(state, ownProps) {
return {
user: state.user
};
}
function mapDispatchToProps(dispatch) {
return {
loadUsers: () => dispatch(userActions.loadUsers())
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
Server throws error "Unhandled Rejection" while rendering Search Component. I had put return statement on both of the case while writing reducers.
Code in Action File:
export const NEWS = "NEWS";
export function news(items) {
const action = {
type: NEWS,
items
}
return action;
}
Code in Reducer File:
import { NEWS } from '../actions';
export default function news
(state = [], action) {
switch(action.type) {
case NEWS:
console.log("News are ",action.items);
return {...state, news: action.items};
default:
return state;
}
}
This is my search function.
class Search extends Component {
constructor(props) {
super(props);
this.state = {
query: ''
};
}
search(){
console.log('Search button clicked', this.state.query);
const url = `http://hn.algolia.com/api/v1/search?query=${this.state.query}`;
// console.log(url);
fetch(url, {
method: 'GET'
}).then(response=> response.json())
.then(jsonObj => {this.props.news(jsonObj.results)});
}
This is NewsResults.js code where I am using mapStateToProps function
import React, { Component } from 'react';
import Search from './Search';
import { connect } from 'react-redux';
class NewsResults extends Component {
render() {
return (
<div>
<h2>
Search Results:
</h2>
<Search/>
</div>
)
}
};
function mapStateToProps(state) {
console.log(state)
return {
news: state.news
}
};
export default connect(mapStateToProps, null)(NewsResults);
** This is what my redux store looks like in index.js**
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import registerServiceWorker from './registerServiceWorker';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>
, document.getElementById('root'));
registerServiceWorker();
The state you are giving to the reducer is a list, to which you are assigning values as if it were an object. change the reducer to have state = initialState = {}
const initialState = {}
export default function (state = initialState, action) {
switch (action.type) {
// your cases
default:
return state
}
}
action creator
export function pickup(latlng) {
return function(dispatch) {
dispatch({ type: PICKUP_STATE,payload:latlng });
};
}
Reducer
import {
PICKUP_STATE,
PICKUP_ADD,
DROPOFF_STATE
} from '../actions/types';
export default (state={},action) => {
const INITIAL_STATE = {
pickup: '',
pickupAdd:''
};
switch(action.type) {
case PICKUP_STATE:
console.log(action.payload)
return {...state,pickup:action.payload};
case PICKUP_ADD:
return{...state,pickupAdd:action.payload};
case DROPOFF_STATE:
return {...state,dropoff:action.payload}
default:
return state;
}
//return state;
}
Map component
import {
connect
} from "react-redux";
import * as actions from "../actions"
class Map extends React.Component {
componentWillReceiveProps(nextprops) {
if (nextprops.pickupProps !== undefined) {
this.setState({
pick: nextprops.pickupProps
}, () => {
console.log(this.state.pick);
});
}
}
isPickEmpty(emptyPickState) {
this.props.pickup(emptyPickState);
// setTimeout(() =>{ console.log('sdkjlfjlksd',this.state.pick)
},3000);
console.log(this.state.pick);
}
}
const mapStateToProps = (state) => {
// console.log(state.BookingData.pickup);
return {
pickupProps:state.BookingData.pickup,
pickupAddProps: state.BookingData.pickupAdd
}
}
export default connect(mapStateToProps,actions)(Map);
app.js root file
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import "normalize.css/normalize.css"
import "./styles/styles.scss";
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import reduxThunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import AppRouter from './routers/AppRouter';
import reducers from './reducers';
import {AUTH_USER} from "./actions/types";
const middleware = [
reduxThunk,
];
const store = createStore(reducers, composeWithDevTools(
applyMiddleware(...middleware),
// other store enhancers if any
));
const token = localStorage.getItem('token');
if(token){
store.dispatch({type:AUTH_USER});
}
ReactDOM.render(
<Provider store={store}>
<AppRouter />
</Provider>
, document.getElementById('app'));
here my problem is when i'm calling isPickEmpty() from my map component
it invoke action creator this.props.pickup(false) (i also checked in redux-devtools it show false value) then i'm consoling pick state( which store in componentWillReceiveProps(nextprops)) so it showing default value instead of false but when i'm consoling the value inside setTimeout(() =>{console.log('sdkjlfjlksd',this.state.pick) }, 3000); it showing false value
correct me if i'm wrong what i know that redux-thunks works in synchronous manner not asynchronous manner so here why it's not working in synchronous manner
i'm stuck,plz anyone help me!
Update
i just got where the prblm, actually in componentWillReceiveProps where i'm setting pick state value because it is asynchronous so when i'm fetching the value in isPickEmpty function i'm getting prev value.
how handle setState or is there any way to solve
At the component you use the values in BookingData, but on the reducer you add it direct to the state.
const mapStateToProps = (state) => {
console.log(state);//Check the state here
return {
pickupProps:state.pickup,
pickupAddProps: state.pickupAdd
}
}
Should work well if you se this mapStateToProps
I've spent a couple of days now searching for the answer to this and I still don't know what I'm doing wrong. I have other projects set up in exactly the same way that are fetching data from api's fine. All other answers have said variations of how the actions need to return objects, which as far as I can tell mine are
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { createLogger } from 'redux-logger';
import Thunk from 'redux-thunk';
import reducer from './reducers/reducer';
import App from './App';
import './css/index.css';
import './css/font-awesome.css';
import './css/bulma.css';
const logger = createLogger();
const store = createStore(reducer, applyMiddleware(Thunk, logger));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>, document.getElementById('root'));
Component calling mapDispatchToProps
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { searchRepositories } from '../actions/searchActions';
class Results extends Component {
componentDidMount() {
this.props.searchRepositories();
}
render() {
return (
<div>Some Stuff</div>
);
}
}
const mapDispatchToProps = (dispatch) => {
return {
searchRepositories: () => {
dispatch(searchRepositories());
},
};
};
const mapStateToProps = (state) => {
return {
repositories: state.repositories,
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Results);
Reducer:
import * as types from '../actions/types';
import initialState from './INITIAL_STATE';
function reducer(prevState = initialState, action) {
if (!action) return prevState;
switch (action.type) {
case types.FETCH_REPOS_REQUEST:
return { ...prevState, loading: true };
case types.FETCH_REPOS_SUCCESS:
return {
...prevState,
loading: false,
repositories: action.data,
error: '',
};
case types.FETCH_REPOS_ERROR:
return { ...prevState, error: 'Encountered an error during GET request' };
default:
return prevState;
}
}
export default reducer;
action creator:
import axios from 'axios';
import * as types from './types';
import { ROOT } from '../config';
export function searchRepositoriesRequest() {
return {
type: types.FETCH_REPOS_REQUEST,
};
}
export function searchRepositoriesSuccess(repositories) {
return {
type: types.FETCH_REPOS_SUCCESS,
data: repositories,
};
}
export function searchRepositoriesError(error) {
return {
type: types.FETCH_REPOS_ERROR,
data: error,
};
}
export function searchRepositories() {
return function (dispatch) {
dispatch(searchRepositoriesRequest());
return axios.get(`${ROOT}topic:ruby+topic:rails`).then((res) => {
dispatch(searchRepositoriesSuccess(res.data));
}).catch((err) => {
dispatch(searchRepositoriesError(err));
});
};
}
I have got this axios api request working using react this.state where I just put it in component did mount. If anyone can see where I am going wrong it would help me out a lot.
Ok, so it looks like I was using an earlier version of redux, which the version of redux-thunk I had installed, didn't like! I updated to the latest version of redux and it now calls as it should.
I'm still not sure why the other projects that have the older version installed still work...
I am using react-router and react-router-redux to handle navigation on my page. I need change my url programmatically inside component. I was trying to use this method: history.push to achieve this but this method is only change the url and component associated with this url is not updated. This app is simple list with pagination so when i switch to the next page url is changing for example /posts/1 to /posts/2 but view is not updated. I think this should work like this:
User click pagination item and click handler is called passing
page number as argument
Inside click handler i call history.push(/posts/[page]). I could
use Link component but i want to be able to do something when user
click pagination item
I expect that my ObjectList component will be mounted again and
componentDidMount will be called
This is probably not the best aproach so i will be greatfull for tips
links are hardcoded especially first argument
My source code:
client.js
import React from "react";
import ReactDOM from "react-dom";
import {Router, Route, IndexRoute, browserHistory} from "react-router";
import Results from "./views/Results";
import Home from "./views/Home";
import App from './components/App'
import { Provider } from 'react-redux';
import store, { history } from './store';
const app = document.getElementById('app');
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="/:category/:cityId/:pageNum" component={Results}></Route>
</Route>
</Router>
</Provider>,
app
);
store.js
import { createStore, compose, applyMiddleware } from 'redux'
import { syncHistoryWithStore } from 'react-router-redux'
import thunkMiddleware from 'redux-thunk'
import { browserHistory } from 'react-router'
import rootReducer from './reducers/index'
import createLogger from 'redux-logger'
import categories from './data/categories'
const loggerMiddleware = createLogger()
const defaultState = {
categories,
resultsList: {
objects: [],
counters: [],
isFetching: false
}
};
const store = createStore(
rootReducer,
defaultState,
compose (
applyMiddleware(
thunkMiddleware,
loggerMiddleware
),
window.devToolsExtension ? window.devToolsExtension() : f => f
)
);
export const history = syncHistoryWithStore(browserHistory, store)
export default store
ObjectList.js
import React from "react";
import ObjectItem from "../components/ObjectItem"
import Loader from "../components/Loader"
import fetchObjects from "../actions/actionCreators";
import switchUrl from "../actions/actionCreators";
import PaginationPanel from "../components/PaginationPanel"
import classNames from 'classnames'
import { push } from 'react-router-redux';
import { browserHistory } from 'react-router'
import store, { history } from '../store';
export default class ObjectList extends React.Component {
static defaultProps = {
objectsPerPage: 20,
objectContainerClassName: 'object_list_items'
}
constructor(props) {
super(props);
}
componentDidMount() {
this.props.fetchObjects(this.props.params.pageNum);
}
paginateHandler(page) {
this.props.history.push('/hotele/1/'+page)
}
render() {
const { resultsList } = this.props
if(resultsList.items.length > 0) {
const ObjectComponents = resultsList.items.map((item) => {
return <ObjectItem key={item.post_id} {...item}/>;
});
const paginationComponent =
<PaginationPanel
{...this.props}
pageNum={Math.ceil(resultsList.counters.allPosts/this.props.objectsPerPage)}
pageClickedHandler={this.paginateHandler.bind(this)}
currentPage={parseInt(this.props.params.pageNum)}
/>
return (
<div className="object-lists">
<div className={this.props.objectContainerClassName}>
<div>{ObjectComponents}</div>
</div>
{paginationComponent}
</div>
)
}
else if(!resultsList.isFetching || resultsList.items.length === 0) {
return <Loader />;
}
}
}
Home.js
import React from "react"
import { Link } from "react-router"
const Home = React.createClass({
render() {
return (
<div>
Strona główna <br />
<Link to={`/hotele/1/1`}>Lista wyszukiwania</Link>
</div>
)
}
})
export default Home
Results.js
import React from "react";
import ObjectList from "../components/ObjectList"
import CategoryTabs from "../components/CategoryTabs"
import fetchObjects from "../actions/actionCreators"
export default class Results extends React.Component{
constructor(props) {
super(props);
}
render() {
return (
<div>
<CategoryTabs { ...this.props } />
<ObjectList { ...this.props } />
</div>
);
}
}
reducers/index.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import objects from './objects'
import categories from './categories'
const rootReducer = combineReducers({objects, categories, routing: routerReducer})
export default rootReducer
reducers/objects.js
function objects(state = {
isFetching: false,
items: [],
counters: []
}, action) {
switch (action.type) {
case 'RECEIVE_OBJECTS':
return Object.assign({}, state, {
isFetching: false,
items: action.objects.posts,
counters: action.objects.counters
})
default:
return state;
}
}
export default objects
app.js
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import * as actionCreators from '../actions/actionCreators';
import Main from '../components/Main';
function mapStateToProps(state) {
return {
resultsList: state.objects,
categories: state.categories
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(actionCreators, dispatch);
}
const App = connect(mapStateToProps, mapDispatchToProps)(Main);
export default App;
actionCreators.js
import fetch from 'isomorphic-fetch'
import { push } from 'react-router-redux';
function receiveObjects(objects, json) {
return {
type: 'RECEIVE_OBJECTS',
objects
}
}
function requestObject(pageNum) {
return {
type: 'REQUEST_OBJECTS',
pageNum
}
}
export function fetchObjects(pageNum) {
return dispatch => {
dispatch(requestObject(pageNum));
let url = 'http://localhost:8080/posts?city=986283&type=hotel&page='+pageNum;
return fetch(url)
.then(response => response.json())
.then(json => dispatch(receiveObjects(json)));
}
}
ObjectList component will not be mounted again because you are not changing components tree. It is still
<Home>
<Results>
<ObjectList />
</Results>
</Home>
It will be remounted only if you go to a different route and mount different root component so the whole tree would change. But You're just passing different props. You need to use
componentWillReceiveProps(nextProps) {
this.props.fetchObjects(nextProps.params.pageNum);
}