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();
}
...
Related
The code works but console log shows it renders twice. I want to reduce unnecessary rerenders/API fetching. Solutions tried include async-await, try-catch, useMemo, React.Memo, StrictMode in index.js (just quadruples the console log entries).
I prepared a codesandbox.
This is the console log:
Console logs twice every time
In the sandbox I use a fake API compared to my actual project code which uses Axios. The outcome is the same.
SearchContext (Context Provider) fetches from API once, then BlogList (Context Consumer) fetches again and console logs the second time.
React Version: 18.2.0
SearchContext.js
import React, { createContext, useState, useEffect, useMemo, useCallback } from 'react';
import axios from 'axios';
const SearchContext = createContext();
export const SearchContextProvider = ({ children }) => {
const [blog, setBlog] = useState([]);
const value = useMemo(() => ([ blog, setBlog ]), [blog]);
// const value = React.memo(() => ([ blog, setBlog ]), [blog]);
// I try 'useMemo, React.memo' (above):
// I try 'try-catch':
// const retrieveBlog = () => {
// try {
// const response = axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
// .then(response => {
// setBlog(response.data.items);
// console.log('thisiscontextprovider(blog):', blog)
// })
// }
// catch (err) {
// console.log(err);
// }
// }
// useEffect(() => {
// retrieveBlog();
// }, []);
// I try 'useCallback, if, async-await, try-catch' :
const retrieveBlog = useCallback(async () => {
if (blog.length === 0){
try {
const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
.then(response => {
setBlog(response.data.items);
console.log('thisiscontextprovider(blog):', blog)
})
}
catch (err) {
console.log(err);
}
}
})
useEffect(() => {
retrieveBlog()
}, []);
// I try 'async-await':
// useEffect(() => {
// async function retrieveBlog() {
// try {
// const response = await axios.get(`${process.env.REACT_APP_API_URL}/api/v2/pages/?type=blog.Blog&fields=image,description`)
// .then(response => {
// setBlog(response.data.items);
// console.log('thisiscontextprovider(blog):', blog)
// })
// }
// catch (err) {
// console.log(err);
// }
// }
// retrieveBlog()
// }, []);
return (
<div>
<SearchContext.Provider value={value}>
{/* <SearchContext.Provider value={[blog, setBlog]}> */}
{children}
{console.log('from context provider return:', blog)}
</SearchContext.Provider>
</div>
);
};
export default SearchContext;
BlogList.js
import React, { useContext, useCallback, useEffect } from 'react';
import styled from 'styled-components';
import SearchContext from "../contexts/SearchContext";
import { SearchContextProvider } from "../contexts/SearchContext";
const BlogCard = styled.div`
`;
const BlogList = () => {
const [blog, setBlog] = useContext(SearchContext);
return (
<div>
<SearchContextProvider>
BlogList
<BlogCard>
{blog.map((blog) => (
<li key={blog.id}>
{blog.title}
</li>
))}
</BlogCard>
</SearchContextProvider>
</div>
);
};
export default BlogList;
App.js
import React from "react";
import { Route, Routes } from "react-router-dom";
import styled from "styled-components";
import BlogList from "./components/BlogList";
import { SearchContextProvider } from "./contexts/SearchContext";
const Wrapper = styled.h1``;
function App() {
return (
<Wrapper>
<p>
This CodeSandbox is for my question on StackOverflow. Console logs
twice.
</p>
<SearchContextProvider>
<Routes>
<Route exact path="" element={<BlogList />} />
{/* <Route exact path="/blog" element={<BlogList />} /> */}
</Routes>
</SearchContextProvider>
</Wrapper>
);
}
export default App;
index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from "react-router-dom";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App />
</BrowserRouter>
);
reportWebVitals();
I am keen to understand why this happens, rather than just solving it.
I am using react router v4 with redux to render some data on the server but i am not able to set the state of the component on the server. Here's my code. Appreciate the Help
heres the server side loadonserver function
loadOnServer({ store, location, routes }).then(() => {
const context = {};
const html = renderToString(
<Provider store={store}>
<StaticRouter location={location} context={context}>
<ReduxAsyncConnect routes={routes} />
</StaticRouter>
</Provider>
);
// handle redirects
if(context.url) {
req.header('Location', context.url)
return res.send(302)
}
// render the page, and send it to the client
res.send(renderLayout(html, '', store.getState(),ApiData , req.protocol + '://' + req.get('x-forwarded-host')));
// render the page, and send it to the client
// can't use until redux-connect works with loadable-components
// getLoadableState(html).then(pageScripts =>
// res.send(renderLayout(html, pageScripts.getScriptTag(), store.getState(), !!(req.user && req.user.isAdmin)))
// )
})
.catch(err => {
console.log(err);
res.status(500).end();
});
ApiData is the data from the server that needs to be set at the server so that the components render out
Heres my index.js
import React from 'react';
import { hydrate } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { BrowserRouter, Route } from 'react-router-dom';
import { ReduxAsyncConnect } from 'redux-connect';
import createHistory from 'history/createBrowserHistory';
import { ConnectedRouter, routerMiddleware, push } from 'react-router-redux';
import routes from './routes';
import reducers from './reducers';
import App from './app';
const initialState = window.__INITIAL_STATE;
const history = createHistory();
const middleware = routerMiddleware(history);
const store = createStore(reducers, initialState, applyMiddleware(middleware));
hydrate(
<Provider store={store}>
<ConnectedRouter history={history}>
<ReduxAsyncConnect routes={routes}/>
</ConnectedRouter>
</Provider>,
document.getElementById('app')
);
Heres my routes.js
import React from 'react';
import App from './app';
import HomePage from './pages/HomePage';
const routes = [{
component: App,
routes: [
{
path : '/',
exact: true,
component: HomePage
}
]
}];
export default routes;
And heres my App.js
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import renderRoutes from 'react-router-config/renderRoutes';
import routes from './routes';
import {isBrowser,isServer} from './util/environmentDetection'
class App extends Component {
constructor(props) {
super(props);
if(isServer) {
} else if(isBrowser && !this.state) {
this.state = window.__DATA;
delete window.__DATA;
}
}
render() {
return (
<div>
<Link to={'/'}>
{'Home'}
</Link>
{renderRoutes(routes[0].routes, { initialData : this.state })}
</div>
);
}
}
export default App;
Here's how I get the request state (typescript). However, I'm not sure how to then get the req.cookies out of the store from my actions, which are needed for the store to be fully populated. Perhaps I'm doing something wrong here.
app.get('*', (req, res) => {
const location = req.url;
const memoryHistory = createMemoryHistory(req.originalUrl);
const store = configureStore(memoryHistory);
const history = syncHistoryWithStore(memoryHistory, store);
match({history, routes, location},
(error, redirectLocation, renderProps) => {
if (error) {
res.status(500).send(error.message);
} else if (redirectLocation) {
res.redirect(302, redirectLocation.pathname + redirectLocation.search);
} else if (renderProps) {
const asyncRenderData = {...renderProps, store, cookies: req.cookies};
loadOnServer(asyncRenderData).then(() => {
const css = [];
const markup = ReactDOMServer.renderToString(
<WithStylesContext onInsertCss={(styles) => css.push(styles._getCss())}>
<Provider store={store} key="provider">
<ReduxAsyncConnect {...renderProps} />
</Provider>
</WithStylesContext>,
);
try {
res.status(200).send(renderHTML(markup, store, css));
} catch (err) {
res.status(500).send('<pre>' + err + '</pre>');
}
}).catch((err) => {
console.log('sending 404', err);
res.status(404).send(x404(JSON.stringify(err || {})));
});
} else {
// https://stackoverflow.com/questions/4483849/default-redirect-for-error-404
res.status(404).send(x404(null));
}
});
});
So I created a RestrictedRoute component based on Route from react-router (v4) and using branch method from recompose:
import React from 'react';
import { connect } from 'react-redux';
import { compose, branch, renderComponent } from 'recompose';
import { Route, Redirect } from 'react-router-dom';
const RestrictedRoute = (props) => {
const { component, ...otherProps } = props;
return <Route {...otherProps} component={component} />;
};
const mapStateToProps = state => ({
authenticated: state.authentication.session,
});
const branched = branch(
({ authenticated }) => !authenticated,
renderComponent(() => <Redirect to="/" />),
);
const enhanced = compose(
connect(mapStateToProps),
branched,
)(RestrictedRoute);
export default enhanced;
It works perfectly fine but now I need to write some tests that will tell me if the redirect is working properl, so I did this:
import React from 'react';
import { shallow } from 'enzyme';
import { Provider } from 'react-redux';
import { Redirect, MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import RestrictedRoute from '../RestrictedRoute';
import { initialAuthenticationState } from 'Reducers/authentication';
describe('<RestrictedRoute />', () => {
const mockStore = configureStore();
let store;
let container;
beforeEach(() => {
store = mockStore({
authentication: { ...initialAuthenticationState }, // authentication.session is false in the initialAuthenticationState
});
container = shallow(
<MemoryRouter>
<Provider store={store}>
<RestrictedRoute />
</Provider>,
</MemoryRouter>
);
})
test('redirects if not authenticated', () => {
expect(container.find(Redirect).length).toBe(1);
});
});
I get the following results, which is not what I expected:
● <RestrictedRoute /> › redirects if not authenticated
expect(received).toBe(expected)
Expected value to be (using ===):
1
Received:
0
What am I missing?
The problem was with the shallow. I shouldn't have used it because it is not its purpose. mount was the function I was looking for.
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);
I am trying to implement a simple form logic for educational purposes. I am stuck trying to redirect to url on form submission. Here are relevant sections of my code;
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import { createStore, combineReducers, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, IndexRoute, hashHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer, routerMiddleware } from 'react-router-redux'
import { reducer as formReducer } from 'redux-form'
import {Home, Foo, Bar} from './components'
import {YirtibatLoginForm as LoginForm} from './containers/LoginForm'
import * as reducers from './reducers'
const reducer = combineReducers({
...reducers,
routing: routerReducer,
form: formReducer
})
const middleware = routerMiddleware(hashHistory)
const store = createStore(reducer, applyMiddleware(middleware))
const history = syncHistoryWithStore(hashHistory, store)
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="foo" component={Foo} />
<Route path="bar" component={Bar} />
<Route path="login" component={LoginForm} />
</Route>
</Router>
</Provider>,
document.getElementById('root')
);
containers/LoginForm.js
import React, { Component } from 'react';
import { connect } from 'react-redux'
import { push } from 'react-router'
import LoginForm from '../components/LoginForm'
export class BaseYirtibatLoginForm extends Component {
constructor() {
super();
this.handlesubmit = this.handlesubmit.bind(this);
}
handlesubmit(ev) {
this.props.submitting();
fetch('/login', {
method:'POST',
body:JSON.stringify(ev)
}).then(resp => {
if(!resp.ok) {
throw new Error(resp.statusText)
}
return resp.json()
}).then( resjson => {
this.props.submitsuccess(resjson)
}).catch(err => {
this.props.submiterror(err);
})
}
render() {
return (
<LoginForm onSubmit={this.handlesubmit} />
);
}
}
const mapStateToProps = (state) => {return {}}
const mapDispatchToProps = (dispatch) => {
return {
submitting: () => dispatch({type:'submitting'}),
submitsuccess: (data) => push("/success"),
submiterror: (err) => push("/error")
}
}
export const YirtibatLoginForm = connect(mapStateToProps, mapDispatchToProps)(BaseYirtibatLoginForm);
I think this code supposed to redirect hash url after the form has been submitted. However I am getting following error in browser console;
Uncaught (in promise) TypeError: (0 , _reactRouter.push) is not a function
at Object.submiterror (LoginForm.js:45)
at LoginForm.js:29
submiterror # LoginForm.js:45
(anonymous) # LoginForm.js:29
What is the prefered method to redirect to a route component after for submission events?
There is no push function exported by react-router. You could work with the history object directly, as mentioned in the comments, but the best way is to use the withRouter higher-order component. The code below touches the key points with inline comments.
// import
import { withRouter } from 'react-router'
...
export class BaseYirtibatLoginForm extends Component {
...
handlesubmit(ev) {
this.props.submitting();
fetch('/login', ...
).then( resjson => {
// take `router` from `this.props` and push new location
this.props.router.push("/success")
}).catch(err => {
// take `router` from `this.props` and push new location
this.props.router.push("/error")
})
}
}
const mapStateToProps = (state) => {return {}}
const mapDispatchToProps = (dispatch) => {
return {
submitting: () => dispatch({type:'submitting'}),
// redirect is not done through redux actions
}
}
// apply withRouter HoC
export const YirtibatLoginForm = withRouter(connect(mapStateToProps, mapDispatchToProps)(BaseYirtibatLoginForm));