How to handle navigation using reactjs and redux - reactjs

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);
}

Related

Can I use dispatch without binding?

I have this code:
Action works and data gets returned correctly.
import axios from 'axios'
export function getBooks(
limit = 10,
start = 0,
order = 'asc'
) {
const req = axios.get(`/api/books?limit=${limit}&skip=${start}&order=${order}`)
.then(res => res.data);
return {
type: 'GET_BOOKS',
payload: req
}
}
In the next file this.props is empty
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getBooks } from '../actions'
class HomeContainer extends Component {
constructor(props) {
super(props);
this.props.dispatch(getBooks(3, 0, 'desc'));
}
render() {
console.log(this.props);
return (
<div>
Home Items
</div>
)
}
}
function mapStateToProps(state) {
return {
book_reducer: state.book_reducer
}
}
export default connect(mapStateToProps)(HomeContainer)
index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import ReduxThunk from 'redux-thunk';
import reducers from './reducers';
import Routes from './routes';
const createStoreWithMiddleware = applyMiddleware(promiseMiddleware, ReduxThunk)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<BrowserRouter>
<Routes/>
</BrowserRouter>
</Provider>
, document.getElementById('root'));
book_reducer.js
export default function(state = {}, action) {
switch(action.type) {
case 'Get_BOOKS':
return {...state, list:action.payload}
default: return state;
}
}
Any idea why this does not work?
you made a typo in the reducer
case 'Get_BOOKS':
it should be
case 'GET_BOOKS': this is what you return from the function

how to use redux in react for get data from api

Hello i am just starting to learn redux and am currently having a problem, i have an api i want to get information from and use it in different components i would appreciate if you help me
import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from "redux-thunk";
import { createLogger } from "redux-logger";
import { BrowserRouter} from "react-router-dom";
import Reducer from './Reducers';
import App from './App';
import fetchSimcards from './Actions/fetchSimcards';
const middleware = [ thunk ];
middleware.push( createLogger() );
const store = createStore(
Reducer
applyMiddleware(...middleware),
);
import * as serviceWorker from './serviceWorker';
store.dispatch(fetchSimcards());
render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
and this is my action file
import * as type from '../Constans/ActionTypes';
export const ReceiveSimcards = Simcards => ({
type: type.RECEIVE_SIMCARDS,
Simcards
});
this is my reducer file
import { combineReducers } from "redux";
const Simcards = ( state = {}, action ) => {
console.log( state, action );
return state;
};
export default combineReducers({
Simcards
});
this is my container file for simcards
import React, {Component} from 'react';
import SimcardList from "../Component/SimcardList";
import { connect } from "react-redux";
class SimcardContainer extends Component {
render() {
const Simcards = this.props;
return (
<div>
<SimcardList title={"Simcards"} />
<div className="TableNumberItem">{Simcards.SimCardNumber}</div>
<div className="TableNumberItem">{Simcards.SimCardDescription}</div>
<div className="TableNumberItem">{Simcards.TeammatePrice}</div>
</div>
);
}
}
export default connect()(SimcardContainer);
and i want show this container in home page
With redux, you should call all API and handling logic code in action.
Example with action fetchAPI:
export const fetchAPI = () = async dispatch => {
let response = null;
try {
response = await axios.get('api/...')
// Example use axios
dispatch(fetchSuccess(response.data))
// To handle in reducer with redux
} catch (error) {
... Handle error here
}
}
const fetchSuccess = data => ({
type: FETCH_SUCCESS,
data: response.data
})
And in your component, you can use connect to get state and action:
import { bindActionCreators } from 'redux';
import React, { Component } from 'react';
import SimcardList from "../Component/SimcardList";
import { connect } from "react-redux";
import * as _Actions from '../../action/index'
class SimcardContainer extends Component {
componentDidMount(){
const { fetchAPI } = this.props.actions;
**fetchAPI();** // Call API here
}
render() {
const { stateReducer} = this.props;
console.log(stateReducer)
// Here, you will see data that you handled in reducer
// with action type FETCH_SUCCESS
// You should remember data that you fetch from API is asynchronous,
// So you should check like that `data && {do any thing heree}`
return (
<div>
<SimcardList title={"Simcards"} />
<div className="TableNumberItem">{Simcards.SimCardNumber}</div>
<div className="TableNumberItem">{Simcards.SimCardDescription}</div>
<div className="TableNumberItem">{Simcards.TeammatePrice}</div>
</div>
);
}
}
const mapStateToProps = state => ({
stateReducer: state
})
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators(_Actions, dispatch)
})
export default connect(mapStateToProps, mapDispatchToProps)(SimcardContainer)

mapStateToProps returning an empty object

mapStateToProps is returning an empty object and then the correct object. This.props.weights is undefined in my component except for inside render
const mapStateToProps = (state) => {
debugger
return {weights: state.fetchWeights}
}
state = {saveWeight: Array(0), fetchWeights: Array(0)}
const mapStateToProps = (state) => {
debugger
return {weights: state.fetchWeights}
}
state = {saveWeight: Array(0), fetchWeights: Array(1)}
import React, { Component } from 'react';
import { connect } from 'react-redux';
import Weight from '../components/Weight.js';
import '../App.css';
class displayWeights extends Component {
constructor(props) {
super(props);
this.state = {weights: this.props.weights};
}
render() {
return (
<div>
</div>
);
}
}
const mapStateToProps = (state) => {
debugger
return {weights: state.fetchWeights}
}
export default connect(mapStateToProps, {})(displayWeights);
-
import fetch from 'isomorphic-fetch';
export function fetchWeights() {
return function(dispatch){
dispatch({type: 'LOADING'})
var url = 'http://localhost:3001/api/v1/weights.json';
var req = new Request(url);
return fetch(req)
.then(function(response) {
return response.json()
})
.then(function(weights) {
debugger
dispatch({type: 'FETCH_WEIGHTS', payload: weights})
})
}
}
export default (state = [], action) => {
switch (action.type) {
case 'FETCH_WEIGHTS':
debugger
return action.payload
default:
return state;
}
}
import { combineReducers } from 'redux';
import saveWeight from './saveWeightReducer.js';
import fetchWeights from './fetchWeightsReducer.js';
export default combineReducers({
saveWeight,
fetchWeights
});
import React, { Component } from 'react';
import logo from './logo.svg';
import { connect } from 'react-redux';
import {
BrowserRouter as Router,
Route
} from 'react-router-dom';
import DisplayWeights from './containers/DisplayWeights.js'
import NewWeight from './containers/NewWeight.js'
import { fetchWeights } from './actions/fetchWeightsAction.js'
import Chart from './components/Chart.jsx';
import './App.css';
class App extends Component {
componentDidMount() {
this.props.fetchWeights()
}
render() {
return (
<div className="App">
<header className="App-header">
<h1>Welcome to Weight Tracker</h1>
<DisplayWeights />
<NewWeight />
<Chart />
</header>
</div>
);
}
}
export default connect(null, {fetchWeights})(App);
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import { store } from './store.js';
import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
registerServiceWorker();
You are triggering the function call to fetchWeights in App component and the response isn't received until the first render of the DisplayWeights components and hence its undefined in the constructor of DisplayWeight. Also you need to assign props to state, if state is directly derivable form props, unless ofcourse you wish to change it locally and then on a submit update it in the props.

React Redux store update rerenders whole app

On onClick SigninButton calls ONTOGGLE_MODAL_SIGNIN which updates ui.isSigninModalActive from store. Everything works fine but I see that my whole app gets re-rendered when ui.isSigninModalActive toggles on and off. Is this normal behaviour? I had thought that you have to store.subscribe and update that component's inner state and that component alone gets updated (and not the whole app) when store updates. If the whole app re-renders, then what is the point of store.subscribe? Or did I mess up somewhere? Thanks for the help in advance.
signin_button.jsx
import React, { Component } from 'react';
import { store } from '../../../store/store';
import { onToggleModal } from '../../../actions/ui';
export const SigninButton = () => (
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" className="signin-button"
onClick={ () => store.dispatch(onToggleModal('signin')) }>
<path d="M0 0h24.997C25.55 0 26 .444 26 1c0 .552-.45 1-1.003 1H0V0"/>
</svg>
);
router.js
import React from 'react';
import { render } from 'react-dom';
import { Router, IndexRoute } from 'react-router';
import { Provider } from 'react-redux';
import { store, history } from '../../store/store';
import App from '../../ui/containers/app_container';
import { Welcome } from '../../ui/pages/welcome';
Meteor.startup(() => {
render(
<Provider store={ store }>
<Router history={ history }>
<Route path="/" component={ App }>
<IndexRoute component={ Welcome } />
</Route>
</Router>
</Provider>,
document.getElementById('root'));
});
store.js
import { createStore } from 'redux';
import { syncHistoryWithStore } from 'react-router-redux';
import { browserHistory } from 'react-router';
import { rootReducer } from '../reducers/root_reducer';
import { ui } from './ui_store';
const defaultState = { ui };
export const store = createStore(rootReducer, defaultState,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
export const history = syncHistoryWithStore(browserHistory, store);
ui_store.js
export const ui = {
isSigninModalActive: false,
};
root_reducer.js
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import { ui } from './ui_reducer';
export const rootReducer = combineReducers({ ui, routing: routerReducer });
ui_reducer.js
import update from 'immutability-helper';
import { toggleBodyOverflow } from '../modules/toggle_body_overflow';
export const ui = (state = null, action) => {
switch (action.type) {
case 'ONTOGGLE_MODAL_SIGNIN': {
toggleBodyOverflow(!state.isSigninModalActive);
document.getElementById('signin-modal__container').classList.toggle('active');
return update(state, { isSigninModalActive: { $set: !state.isSigninModalActive } });
}
default: return state;
}
};
ui_action.js
export const onToggleModal = modal => ({ type: `ONTOGGLE_MODAL_${modal.toUpperCase()}` });
EDIT: I found the reason why app is re-rendering
On my app container, I have set mapStateToProps and sent the state.ui down the components as props. I "fixed" it by removing it. Is this the correct way to stop re-rendering the whole app?
app_container.js
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as ui from '../../actions/ui';
import { App } from '../layouts/app_layout';
// problem: const mapStateToProps = state => { ui: state.ui };
const mapStateToProps = () => ({});
const mapDispatchToProps = dispatch => bindActionCreators(ui, dispatch);
export default connect(mapStateToProps, mapDispatchToProps)(App);

react-router-redux does not change content/view

I have the react-router-redux set up as this example. But dispatch(push(url)) does not change the content/view nor the url on address bar. Even though from my console I can see that the LOCATION_CHANGE and CALL_HISTORY_METHOD are successfully called with my given address. In this case, if the sign in is success, it does not load the expected redirect address.
a sign in action
export function loginUser(email, password, redirect="/dashboard") {
return function(dispatch) {
dispatch(loginUserRequest());
return fetch(`http://localhost:3000/users/sign_in/`, {
...
})
.then(parseJSON)
.then(response => {
try {
let decoded = jwtDecode(response.token);
dispatch(loginUserSuccess(response.token));
dispatch(push(redirect));
} catch (e) {
...
}
})
}
}
routes.js
import React from 'react';
import { Router, Route, browserHistory, IndexRoute } from 'react-router';
import { syncHistoryWithStore } from 'react-router-redux'
...
import store from '../store';
const history = syncHistoryWithStore(browserHistory, store)
let Routes =
<Router history={history}>
<Route path='/' component={MainContainer}>
<IndexRoute component={Home} />
<Route path='/sign_in' component={SigninForm} />
<Route path='dashboard' component={authenticateComponent(Dashboard)} />
</Route>
</Router>
export default Routes;
store.js
import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import reducers from './reducers';
const createStoreWithMiddleware = compose(
applyMiddleware(
thunkMiddleware,
createLogger()
)
)
const store = createStore(reducers, createStoreWithMiddleware);
export default store;
AuthenticateComponent.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
export function authenticateComponent(Component) {
class AuthenticateComponent extends Component {
componentWillMount () {
this.checkAuth(this.props.isAuthenticated);
}
componentWillReceiveProps (nextProps) {
this.checkAuth(nextProps.isAuthenticated);
}
checkAuth (isAuthenticated) {
if (!isAuthenticated) {
let redirectAfterLogin = this.props.location.pathname;
this.props.dispatch(push(`/sign_in?next=${redirectAfterLogin}`));
}
}
render () {
return (
<div>
{this.props.isAuthenticated === true
? <Component {...this.props}/>
: null
}
</div>
)
}
}
const mapStateToProps = (state) => ({
token: state.auth.token,
isAuthenticated: state.auth.isAuthenticated
});
return connect(mapStateToProps)(AuthenticateComponent);
}
I have put the routing: routerReducer into my combined reducers as well. What could be the problem of this?
Turns out I need to apply the routerMiddleware as well

Resources