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));
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();
}
...
I am trying to setup an test file to render a route/page on my application. I'm trying to wrap everything with Redux and Router, and this is what I have:
import React from 'react';
import { render } from 'react-testing-library';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import reducer from '../../store/reducer';
import {Link, Route, Router, Switch} from 'react-router-dom'
import {createMemoryHistory} from 'history'
import ViewNode from '../Pages/ViewNode';
const customRender = (
ui,
{
route = '/',
history = createMemoryHistory({ initialEntries: [route] }),
initialState,
store = createStore(reducer, initialState),
...options
} = {}
) => ({
...render(
<Provider store={store}>
<Router history={history}>{ui}</Router>
</Provider>,
options
),
history,
});
test('can render with redux and router', () => {
const { getByTestId } = customRender(
<Route path="/server/:env/:nodeName">
<ViewNode />
</Route>,
{
route: '/server/prod/some.server.name.com',
}
);
expect(getByTestId('page-content')).toBeVisible()
})
Then I get the following error:
Error: Uncaught [TypeError: Cannot read property 'params' of undefined]
The reason this is throwing the error is because it cannot find the React Router params. It's failing in the component constructor when Im initializing the state:
this.state = {
modal: false,
activeTab: '1',
imageStatus: "loading",
env: props.match.params.env, //failing here
nodeName: props.match.params.nodeName,
environments: props.environments,
}
It seems like it isn't wrapping the router properly with my implementation above.
How would I properly wrap my page component with Redux and Router so that it can get these router params?
You have placed your <ViewNode /> component inside a Route but forgot to pass on the props it receives. That is why props.match is undefined in your component.
You can do this instead:
<Route path="/server/:env/:nodeName">
{props => <ViewNode {...props} />}
</Route>
Basically, you can use one of the 3 ways to render something with a <Route>.
Here is a working example:
import React from 'react'
import {Route, Router} from 'react-router-dom'
import {createMemoryHistory} from 'history'
import {render, fireEvent} from '#testing-library/react'
import {createStore} from 'redux'
import {Provider, connect} from 'react-redux'
function reducer(state = {count: 0}, action) {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1,
}
case 'DECREMENT':
return {
count: state.count - 1,
}
default:
return state
}
}
class Counter extends React.Component {
increment = () => {
this.props.dispatch({type: 'INCREMENT'})
}
decrement = () => {
this.props.dispatch({type: 'DECREMENT'})
}
render() {
return (
<div>
<div data-testid="env-display">{this.props.match.params.env}</div>
<div data-testid="location-display">{this.props.location.pathname}</div>
<div>
<button onClick={this.decrement}>-</button>
<span data-testid="count-value">{this.props.count}</span>
<button onClick={this.increment}>+</button>
</div>
</div>
)
}
}
const ConnectedCounter = connect(state => ({count: state.count}))(Counter)
function customRender(
ui,
{
initialState,
store = createStore(reducer, initialState),
route = '/',
history = createMemoryHistory({initialEntries: [route]}),
} = {},
) {
return {
...render(
<Provider store={store}>
<Router history={history}>{ui}</Router>
</Provider>,
),
store,
history,
}
}
test('can render with redux and router', () => {
const {getByTestId, getByText} = customRender(
<Route path="/server/:env/:nodeName">
{props => <ConnectedCounter {...props} />}
</Route>,
{
route: '/server/prod/some.server.name.com',
},
)
expect(getByTestId('env-display')).toHaveTextContent('prod')
expect(getByTestId('location-display')).toHaveTextContent(
'/server/prod/some.server.name.com',
)
fireEvent.click(getByText('+'))
expect(getByTestId('count-value')).toHaveTextContent('1')
})
This is how I tested my routes.
You use react-redux for the Provider
You create the initial state for your store
add it to your provider
now you can select elements, expect them to match your html (per example)
import { render } from '#testing-library/react';
import { Router, Switch, Route } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { Provider } from 'react-redux';
import React from 'react';
import createStore from 'redux-mock-store';
jest.mock('../../components/Form/ManagerSelect', () => jest.fn(() => null));
describe('router page', () => {
const createState = state => {
return {
//whatever u need
}
};
const Home = _ => <span>home</span>;
const Profile = _ => <span>profile</span>;
const renderComponent = state => {
const store = createStore()(state);
//this is the "history" of your app like:
// homepage -> about -> contact -> cart page ...
const initialEntries = ['/'];
return render(
<Provider store={store}>
<Router history={createMemoryHistory({ initialEntries })}>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/profile" component={Profile} />
</Switch>
</Router>
</Provider>
);
};
it('missing emergency details should redirect to profile', () => {
const rendered = renderComponent(createState());
expect(rendered.container.innerHTML).toEqual('<span>profile</span>');
});
});
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.
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