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);
Related
I am developing a React JS, Redux, GraphQL, TypeScript app.
And I would like to know how to invoke the function that fetches data and updates the state via GraphQL from my container.
The name of the action that loads the data via GraphQL is appActions.getAppData();
But it causes an infinite refresh loop because it triggers (StatusActions.startAppLoading()); which updates the state as well.
I would like to know how to fix this issue or how to rewrite /Main/index.tsx as a class component and invoke startAppLoading() from componentDidMount().
Thank you in advance.
main.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { configureStore } from 'app/store';
import { Router } from 'react-router';
import { App } from './app';
// prepare store
const history = createBrowserHistory();
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>,
document.getElementById('root')
);
app/index.tsx
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import { App as Main } from 'app/containers/Main';
import { hot } from 'react-hot-loader';
let currentContainer = Main;
export const App = hot(module)(() => (
<Switch>
<Route exact path="/" component={currentContainer} />
<Route path="*">
<Redirect to="https://google.com" />
</Route>
</Switch>
));
app/containers/Main/index.tsx
import React from 'react';
import style from './style.css';
import { RouteComponentProps } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { useTodoActions } from 'app/actions';
import { useAppActions } from 'app/actions';
import { RootState } from 'app/reducers';
import { Header, TodoList, Footer } from 'app/components';
export namespace App {
export interface Props extends RouteComponentProps<void> {}
}
export const App = ({ history, location }: App.Props) => {
const dispatch = useDispatch();
const appActions = useAppActions(dispatch);
const { apps } = useSelector((state: RootState) => {
return {
apps: state.apps
};
});
appActions.getAppData();
return (
<div className={style.normal}>
<Header />
<TodoList appActions={appActions} apps={apps} />
<Footer />
</div>
);
};
app/actions/apps.ts
export const getAppData = () => {
let appKey = 'interpegasus';
return (dispatch: Dispatch) => {
dispatch(StatusActions.startAppLoading());
debugger;
apolloClient
.query({
query: gql`
query getApp($appKey: String!) {
getApp(id: $appKey) {
id
name
domain
}
}
`,
variables: {
appKey: appKey
}
})
.then((result) => {
debugger;
if (result.data.apps.length > 0) {
dispatch(populateAppData(result.data.apps[0]));
}
dispatch(StatusActions.endAppLoading());
})
.catch((error) => {
dispatch(StatusActions.endAppLoading());
console.log({
error: error
});
});
};
};
You should put your appActions.getAppData() inside useEffect hooks like this
useEffect(()=>{
appActions.getAppData()
},[])
check the official docs Introducing Hooks
In Main/index.tsx, you are calling appActions.getAppData(); which will lead you to actions/apps.ts. Here, you are doing dispatch(StatusActions.startAppLoading()); which will update the state and re-render ``Main/index.tsx`. Then again you call getAppData() and the loop continues to lead to infinite loop.
Call the api only if not loading.
Something like this:
...
const { apps, loading } = useSelector((state: RootState) => {
return {
apps: state.apps,
loading: state.loading // <----- replace with your actual name of your state
};
});
if(!loading){
appActions.getAppData();
}
...
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)
I would like my saga to redirect a logging in user to a main dashboard screen after successful authentication. I followed the code outlined in the following article: https://github.com/supasate/connected-react-router and example from https://github.com/supasate/connected-react-router/tree/master/examples/basic. I can see the url change when the push command is called from within the saga, however, the page is not redirecting to the dashboard screen. Instead, the user just stays on the same screen. I am currently running react-redux v6, react v16.4.1, react-router: v4.3.1, react-router-redux v4.0.8. What could I be doing wrong?
app.js:
class App extends Component {
constructor() {
super();
this.state = {
loading: true,
loaded: false
}
}
componentDidMount() {
window.addEventListener('load', () => {
this.setState({loading: false});
setTimeout(() => this.setState({loaded: true}), 500);
});
}
render() {
const loaded = this.state.loaded;
const {history} = this.props;
return (
<div>
{!loaded && <div className={`load${this.state.loading ? '' : ' loaded'}`}>
<div className='load__icon-wrap'>
<svg className='load__icon'>
<path fill='#4ce1b6' d='M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z'/>
</svg>
</div>
</div>}
<div>
<ConnectedRouter history={history}>
<Router/>
</ConnectedRouter>
</div>
</div>
)
}
}
history.js:
import { createBrowserHistory } from 'history';
export default createBrowserHistory;
store.js:
import {createStore, applyMiddleware} from 'redux';
import {reducer as reduxFormReducer} from 'redux-form';
import createSagaMiddleware from 'redux-saga';
import sagas from '../redux/sagas';
import reducers from '../redux/reducers';
import { createBrowserHistory, } from 'history';
import { routerMiddleware } from 'connected-react-router';
const history = createBrowserHistory({ basename: '/myrootpath' });
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers(history), applyMiddleware(routerMiddleware(history), sagaMiddleware));
store.runSaga = sagaMiddleware.run(sagas);
// store.asyncReducers = {};
const action = type => store.dispatch({type })
export default store;
index.js
import React from 'react';
import App from './app/App';
import {render} from 'react-dom'
import {Provider} from 'react-redux'
import {BrowserRouter} from 'react-router-dom';
import store from './app/store';
import ScrollToTop from './app/ScrollToTop';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory()
render(
<Provider store={store}>
<BrowserRouter basename='/myrootpath'>
<ScrollToTop>
<App history={history}/>
</ScrollToTop>
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
saga code:
function* login(params){
try {
const api = new UserApi();
const {username, password} = params.login;
const user = yield call(api.login, {username, password});
debugger;
yield put(push('/dashboard_default'));
} catch (error) {
debugger;
yield put({ type: types.LOAD_ERROR, error });
}
}
The same history object needs to be used in both places, e.g. in history.js:
export default createBrowserHistory({basename: '/myrootpath'});
and in index.js and store.js:
import history from './history';
I am learning redux. I have written an application for the same, first time. I am not able to understand why data isnt being fetched from store. Is there anything wrong with my connect call? Please help.
I am getting error that contactsList is undefined while calling map below, although connect should fetch that list as per my understanding.
ContactCards.js
import React, { Component } from 'react';
import Card from './Card';
import { connect } from 'react-redux';
const CardList = ({ contactsList }) => {
const cardsArray = contactsList.map(contact => (
<Card
key={contact.id}
name={contact.name}
email={contact.email}
phone={contact.phone}
website={contact.website}
city={contact.address.city}
companyName={contact.company.name}
id={contact.id} />
));
return (
<div className="jumbotron">
<div className='card-columns'>
{cardsArray}
</div>
</div>
);
};
const mapStateToProps = state => {
return { contactsList: state.contactsList };
};
const ContactCards = connect(mapStateToProps)(CardList);
export default ContactCards;
Reducer dataReducer
import ContactsList from '../components/MockContactsList';
import {loadContacts, updateContacts} from '../Actions/CardActions';
const INITIAL_STATE = {
contactsList: ContactsList
};
const dataReducer = (state = INITIAL_STATE, action) => {
switch(action.type) {
case loadContacts:
return state;
case updateContacts:
const updatedItems = state.contactsList.map(item => {
if(item.id === action.id){
return { ...item, ...action.payload }
}
return item
})
return updatedItems;
default:
return state;
}
};
export default dataReducer;
App.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import registerServiceWorker from './registerServiceWorker';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import reducers from './reducers/index';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const store = createStore(reducers, {});
ReactDOM.render(
<Provider store = {store}>
<App />
</Provider>
,document.getElementById('root'));
registerServiceWorker();
Reducers index.js
import { combineReducers } from 'redux';
import dataReducer from './dataReducer';
export default combineReducers({
dataReducer
});
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/App';
import registerServiceWorker from './registerServiceWorker';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import reducers from './reducers/index';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const store = createStore(reducers, {});
ReactDOM.render(
<Provider store = {store}>
<App />
</Provider>
,document.getElementById('root'));
registerServiceWorker();
You must first access your reducer state (dataReducer) then its property:
const mapStateToProps = state => {
return { contactsList: state.dataReducer.contactsList };
};
In your store you have a key of dataReducer:
export default combineReducers({
dataReducer
});
But you're accessing it as contactsList here:
const mapStateToProps = state => {
return { contactsList: state.contactsList };
};
So you probably need to have that key in your store instead:
export default combineReducers({
contactsList: dataReducer
});
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);
}