I have a fully working setup of React with Redux. In my action file, I use axios to make calls to 3rd party API.
After connecting my form component to the Redux store and trying to call the POST action, I am presented with an error saying that I cannot call then on undefined...Funny thing is that the action is still called i.e. the form is submitted to the 3rd party's api right after this console error is thrown.
I have looked at numerous S/O questions as well as the axios & redux-thunk docs, but can't see what I'm missing. Here is some of the relevant code:
actions.js
import axios from 'axios';
import _ from 'lodash'
import runtimeEnv from '#mars/heroku-js-runtime-env';
import {apiErrorCatcher} from "../../../utils/errorCatcher"
import {toastr} from 'react-redux-toastr'
const env = runtimeEnv();
export function createProfile(data) {
return dispatch => {
axios.post( env.REACT_APP_API_URL + '/api/profile/', data).then(
(res) => {
toastr.success('Profile added', 'Congratulations on adding your profile')
return res
}
)
.catch(function (error) {
apiErrorCatcher(error)
toastr.error('Oops...', 'Unfortunately an error occurred on our side. Please try again later.')
return error
})
}
}
Profile.js
...
handleCreateProfile=()=>{
this.props.actions.createProfile(this.state.data).then(
(res) => {console.log(res)}
)
}
...
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators({createProfile}, dispatch) }
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Profile))
index.js
...
const store = createStore(
rootReducer,
composeWithDevTools(
applyMiddleware(thunk, oidcMiddleware)
)
);
...
ReactDOM.render(
<Provider store={store}>
<OidcProvider store={store} userManager={userManager}>
<BrowserRouter>
<MuiThemeProvider>
<App />
</MuiThemeProvider>
</BrowserRouter>
</OidcProvider>
</Provider>,
document.getElementById('root')
);
I think that the error might have something to do with promises but I am not 100% confident. Also not sure how to resolve this as I can't see where I'm going wrong.
Problem is in your component method:
this.props.actions.createProfile(this.state.data).then(
(res) => {console.log(res)}
)
becouse action don't return promise, so if you want log your data, or dispatch action you should do it in action createProfile.
Related
This is my component.
const DashboardContext = React.createContext()
function DashboardStateProvider({ children }) {
const Provider = DashboardContext.Provider
return (
<WithSubscription Provider={Provider} connectedAccount={wallet.account}>
{children}
</WithSubscription>
)
}
async function WithSubscription({ Provider, connectedAccount, children }) {
const { data } = await asyncCallHere()
return ( <Provider value={{ treasury }}> {children} </Provider> )
}
function useDashboardState() {
return useContext(DashboardContext)
}
export { DashboardStateProvider, useDashboardState }
In the code, one can see asyncCallHere. Before this code, asyncCallHere was synchronous which means I had no mistake in the code. But I need it to make async, so I had to add async to withSubscription function. Hope the code makes sense.
The problem is that because I put await and async there, it results in the following error:
Unhandled Rejection (Error): Invalid hook call. Hooks can only be
called inside of the body of a function component. This could happen
for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app See facebook dev page for tips about how to debug and
fix this problem.
I'd appreciate the quick answer to fix this, due to the fact that I am not react developer and I don't intend to understand what's happening there in deep other than to fix it.
Hooks cannot be async.
You always need to return sync data from the hook.
However, if you want to do some async operation inside the hook, you can do it inside useEffect to store the data in the hook state and return that data from the hook itself.
import React, {useEffect, useState} from 'react'
function useAsyncCallHere() {
const [data, setData] = useState(null)
useEffect(() => {
async function getData() {
const response = await asyncCallHere()
setData(response.data)
}
getData()
}, [])
return data
}
function WithSubscription({ Provider, connectedAccount, children }) {
const data = useAsyncCallHere()
return ( <Provider value={{ data }}> {children} </Provider> )
}
In my application on Next.Js i use redux and redux saga. I want to use ssr making http requests:
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getDataRequest());
store.dispatch(END);
await store.sagaTask.toPromise();
});
In the same time i want to get data of the above result:
const selector = useSelector((s) => s);
console.log(selector);
The issue is that, when i run the application i get an error:
Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>
I used the provider but the data doesn't appear. Question: How to solve the issue in my application?
demo: https://codesandbox.io/s/awesome-butterfly-f7vgd?file=/pages/store/saga.js
this is your _app component:
function App({ Component, pageProps }) {
return (
<div>
<Provider store={makeStore}>
<Component {...pageProps} />
</Provider>
</div>
);
}
you dont need to wrap it with Provider. this is the only thing you need to do in _app.
export default wrapper.withRedux(App)
this is getStatisProps in your pages/index.js
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getDataRequest());
store.dispatch(END);
await store.sagaTask.toPromise();
});
see store is already passed here. You will be accesing state via store.getState(). this is how it should be
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
store.dispatch(getDataRequest());
store.dispatch(END);
await store.sagaTask.toPromise();
const state = store.getState();
return {props:{data:state}} // since you have only one reducer
});
now data will be passed as a prop to your component. if you console.log(props.data) inside the component, you should be seeing your dataReducer
I've connected presentational-functional component to the redux store by means of connect function.
export default connect(
state => ({tasks: state.tasks}),
dispatch => ({
getTasks: () => apiGetTasks(dispatch),
getTask: (id) => apiGetTask(dispatch, id),
})
) (TaskView)
And now I want to redirect on the other page when getTask has triggered (or even better when apiGetTask has finished). So I tried to use history in the next way:
export default connect(
state => ({tasks: state.tasks}),
dispatch => ({
getTasks: () => apiGetTasks(dispatch),
getTask: (id) => apiGetTask(() => {
const history = useHistory()
dispatch(id)
history.push('/otherPage')
}, id),
})
) (TaskView)
But it does not work with the message:
Unhandled Rejection (Error): Invalid hook call. Hooks can only be called inside of the body of a function component....
I see that it is wrong pattern probably... So, what is the best one? Do I need to pass callback from parent component and make redirect in it? Or may be I should redirect in presentational component (but it looks strange for presentational component)
Or may be I should call dispatch(switchToPage()) in apiGetTask's promise and implement redirection in the application component, basing on the value?
What is the best solution in such case?
And one related question yet: Is it regular to use api calls in such manner as above?
Thank you, in advance!
Well.
Now you can assess my solution:
---index.js----
export let history = null;
const DefaultComponent = ()=> {
history = useHistory()
return <div style={{visibility: 'hidden'}}/>
}
ReactDOM.render(
<React.StrictMode>
<Provider store={createAppStore()}>
<Router>
<DefaultComponent/>
<Dashboard />
</Router>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
---Tasks.js----
import {history} from './index.js'
export default connect(
state => ({tasks: state.tasks}),
dispatch => ({
getTasks: () => apiGetTasks(dispatch),
getTask: (id) => apiGetTask(dispatch, id, ()=>history.push('/CreateTask')),
})
) (TaskView)
Don't hesitate to criticise. Any proposals are welcome!
I have read lots of similar questions,And no one fit for me.
about
this.props.children doesn't always inherit the context from its parent
Could not find "store" in either the context or props
//store config
const createHistoryWithBasename = (historyOptions) => {
return useBasename(createHistory)({
basename: '/user_home'
})
}
const finalCreateStore = compose(
applyMiddleware(thunk, createLogger()),
reduxReactRouter({
createHistory: createHistoryWithBasename }),
devTools()
)(createStore);
export function configureStore(initialState) {
const store = finalCreateStore(rootReducer, initialState);
return store;
}
the index file, all route nested in Provider.
//entry file
let routes = (
<Route path="/" component={UserHome}></Route>
);
ReactDom.render(
<Provider store={store} >
<ReduxRouter>
{routes}
</ReduxRouter>
</Provider>,
rootElement
);
the Component. I logged the this.props, there are't the dispatch property. And the parameter of context is an empty object.
//user_home.js
class UserHome extends React.Component {
constructor(props, context){
//context is empty Object
super(props, context);
}
render() {
//this.props.dispatch is undefined
return (
<div className="container">
test
</div>
);
}
}
function mapStateToProps(state){
return state;
}
export default connect(mapStateToProps, {
pushState
})(UserHome);
Maybe, it's a simple question, but It took me a lot of time.Hope to get your help. Thank you!
Maybe we can inject the action creator to connect
import * as ActionCreators from './actions/user_actions';
//we can use the actions in ActionCreators to dispatch the action.
export default connect(mapStateToProps, {
pushState: pushState,
...ActionCreators
})(Component);
the actions
//action file
export function xxAction(){
return dispatch =>{
dispatch({
type: 'XX_ACTION'
});
}
}
this solution fit for dispatching actions in children component.There are some limitations, Looking forward to a better solution.
I have a pretty simple set of react components:
container that hooks into redux and handles the actions, store subscriptions, etc
list which displays a list of my items
new which is a form to add a new item to the list
I have some react-router routes as follows:
<Route name='products' path='products' handler={ProductsContainer}>
<Route name='productNew' path='new' handler={ProductNew} />
<DefaultRoute handler={ProductsList} />
</Route>
so that either the list or the form are shown but not both.
What I'd like to do is to have the application re-route back to the list once a new item has been successfully added.
My solution so far is to have a .then() after the async dispatch:
dispatch(actions.addProduct(product)
.then(this.transitionTo('products'))
)
Is this the correct way to do this or should I fire another action somehow to trigger the route change?
If you don't want to use a more complete solution like Redux Router, you can use Redux History Transitions which lets you write code like this:
export function login() {
return {
type: LOGGED_IN,
payload: {
userId: 123
}
meta: {
transition: (state, action) => ({
path: `/logged-in/${action.payload.userId}`,
query: {
some: 'queryParam'
},
state: {
some: 'state'
}
})
}
};
}
This is similar to what you suggested but a tiny bit more sophisticated. It still uses the same history library under the hood so it's compatible with React Router.
I ended up creating a super simple middleware that roughtly looks like that:
import history from "../routes/history";
export default store => next => action => {
if ( ! action.redirect ) return next(action);
history.replaceState(null, action.redirect);
}
So from there you just need to make sure that your successful actions have a redirect property. Also note, this middleware does not trigger next(). This is on purpose as a route transition should be the end of the action chain.
For those that are using a middleware API layer to abstract their usage of something like isomorphic-fetch, and also happen to be using redux-thunk, you can simply chain off your dispatch Promise inside of your actions, like so:
import { push } from 'react-router-redux';
const USER_ID = // imported from JWT;
function fetchUser(primaryKey, opts) {
// this prepares object for the API middleware
}
// this can be called from your container
export function updateUser(payload, redirectUrl) {
var opts = {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
};
return (dispatch) => {
return dispatch(fetchUser(USER_ID, opts))
.then((action) => {
if (action.type === ActionTypes.USER_SUCCESS) {
dispatch(push(redirectUrl));
}
});
};
}
This reduces the need for adding libraries into your code as suggested here, and also nicely co-locates your actions with their redirects as done in redux-history-transitions.
Here is what my store looks like:
import { createStore, applyMiddleware } from 'redux';
import rootReducer from '../reducers';
import thunk from 'redux-thunk';
import api from '../middleware/api';
import { routerMiddleware } from 'react-router-redux';
export default function configureStore(initialState, browserHistory) {
const store = createStore(
rootReducer,
initialState,
applyMiddleware(thunk, api, routerMiddleware(browserHistory))
);
return store;
}
I know I am little late in the party as react-navigation is already included in the react-native documentation, but still it can be useful for the user who have used/using Navigator api in their apps.
what I tried is little hackish, I am saving navigator instance in object as soon as renderScene happens-
renderScene(route, navigator) {
const Component = Routes[route.Name]
api.updateNavigator(navigator); //will allow us to access navigator anywhere within the app
return <Component route={route} navigator={navigator} data={route.data}/>
}
my api file is something lke this
'use strict';
//this file includes all my global functions
import React, {Component} from 'react';
import {Linking, Alert, NetInfo, Platform} from 'react-native';
var api = {
navigator,
isAndroid(){
return (Platform.OS === 'android');
},
updateNavigator(_navigator){
if(_navigator)
this.navigator = _navigator;
},
}
module.exports = api;
now in your actions you can simply call
api.navigator.push({Name:'routeName',
data:WHATEVER_YOU_WANTED_TO_PASS)
you just need to import your api from the module.
If you're using react-redux and react-router, then I think this link provides a great solution.
Here's the steps I used:
Pass in a react-router history prop to your component, either by rendering your component inside a react-router <Route/> component or by creating a Higher Order Component using withRouter.
Next, create the route you want to redirect to (I called mine to).
Third, call your redux action with both history and to.
Finally, when you want to redirect (e.g., when your redux action resolves), call history.push(to).