How to call useDispatch in a callback - reactjs

I got a React component which is trying to get some data, and is calling an onSuccess(result) call back upon a successful retrieval of data.
I need to save the data to redux. I created custom hooks which are using useDispatch, and I'm trying to do something like this:
<MyComponent onSuccess = {res => myCustomHook(res)} />
but I get an error because an hook can not be called inside a callback.
I know that hooks can only be called at the top level of a functional component.. So how can I achieve what I need?
The custom hook:
export function useSaveData(type, response)
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
const dispatch = useDispatch();
dispatch(actionCreator.addAction(type, obj));
}

The parent component could pass dispatcher to useSaveData as follows.
export const useSaveData = (dispatch) => (type, response) =>
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
dispatch(actionCreator.addAction(type, obj));
}
And parent component becomes;
function ParentComponent() {
const dispatch = useDispatch();
const myCustomHook = useSaveData(dispatch);
return <MyComponent onSuccess = {res => myCustomHook(ACTION_TYPE, res)} />
}

Your custom hook should return a function which can be passed as callback.
useMyCustomHook = () => {
// your hook
const onSuccess = () => {
// save data here
}
return { onSuccess };
}
In your component.
function MyComponent(props) {
const { onSuccess } = useMyCustomHook();
// your component code, you have onSuccess which can be bind with button or form as per your requirement.
}
Edit after seeing your custom hook.
You don't need to create custom hook here. you can simply pass dispatch to your callback.
const dispatch = useDispatch()
<MyComponent onSuccess={res => onSuccess(res, dispatch)} />
create onSucces function.
export function onSuccess(type, response, dispatch)
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
dispatch(actionCreator.addAction(type, obj));
}

useDispatch is a React Redux hook, and can not be directly used anywhere outside of the functional component's body. You would not be allowed to even directly use it within another plain javascript function, even if it was getting invoked from the functional component's body. So, to use the useDispatch hook elsewhere, a const can be declared out of the useDispatch hook.Please note that, this should only happen from within your functional component like below:
export default function MyComponent {
const dispatch = useDispatch()
......
}
Now, this const dispatch can be passed around anywhere else, including a call back. Within your call back, you may access the passed dispatch argument and invoke redux APIs on it, like below:
export function useSaveData(type, response, dispatch)
{
if(!type|| !response)
{
throw new Error("got wrong parameters for useSaveData");
}
let obj= {
myData1: response.data1,
myData2: response.data2
};
sessionStorage.setItem(type, JSON.stringify(obj));
dispatch(actionCreator.addAction(type, obj));
}

Related

Call action from Redux store, to change React component state

I have the following action, making an asynchronous GET request:
export const getPolls = () => {
return async dispatch => {
try {
const polls = await serv.call('get', 'poll');
dispatch(setPolls(polls));
dispatch(removeError());
} catch (err) {
const error = err.response.data;
dispatch(addError(error.message));
}
}
}
Then, in component Polls, I want to be able to call this action, so I can show the list of Polls. To do this, I pass it on to this component's props:
export default connect(store => ({
polls: store.polls
}), {
getPolls
})
(Polls);
And access it through const {getPolls} = props;
I am using React Hooks to create and change the Polls component state. Like this:
const Polls = (props) => {
const {getPolls} = props
const [polls, setPolls] = useState([])
useEffect(() => {
const result = getPolls()
console.log(result)
setPolls(result)
}, [])
const pollsList = polls.map(poll => (<li key={poll._id}>{poll.question}</li>))
return (
<div>
<ul className='poll-list'>{pollsList}</ul>
</div>
)
}
With this code, I'm not able to get the polls. When I console.log the result from calling getPolls(), I can see I'm obtaining a Promise. However, since getPolls() is an async function, shouldn't this be avoided? I believe the problem has something to do with the way I'm using React Hooks, particularly useEffect, but I can't figure it out.
Thank you.
When I console.log the result from calling getPolls(), I can see I'm obtaining a Promise. However, since getPolls() is an async function, shouldn't this be avoided?
You have a fundamental misunderstanding of async functions. async and await are just syntaxes that help you deal with Promises. An async function always returns a Promise. In order to get an actual value, you would have to await the value from inside another async function.
But your getPolls() function is not a function that returns the polls. It doesn't return anything. It fetches the polls and calls dispatch with the data to store the polls in your redux store.
All that you need to do in your component is call getPolls so that this code is executed. Your connect HOC is subscribing to the current value of polls from store.polls and the polls prop will update automatically when the getPolls function updates store.polls (which it does by calling dispatch(setPolls(polls))).
Your component can be simplified to this:
const Polls = (props) => {
const {polls, getPolls} = props;
// effect calls the function one time when mounted
useEffect(() => {
getPolls();
}, [])
const pollsList = polls.map(poll => (<li key={poll._id}>{poll.question}</li>))
return (
<div>
<ul className='poll-list'>{pollsList}</ul>
</div>
)
}

Dispatching redux action in axiosinterceptor (Non-react component)

is it possible to dispatch a redux function from a javascript object that is not part of react's component?
Usually when i wish to dispatch an action , i would do the following:
I'll map the dispatch function to the component's props :
const mapDispatchToProps = dispatch => {
return {
autoCheckState: () => dispatch(actions.authCheckState()),
}
}
And then i can just call the dispatch function like this:
async componentDidMount() {
await this.props.autoCheckState();
this.setState({
checking:false
})
}
However , now i wish to call the dispatch function from within my axiosinterceptor object , and they don't have "props"
Here is what i envisioned would be the way to use it , but ofcourse it does not work:
axiosInstance.interceptors.response.use(
response => {
return response
},error => {
######## bunch of code that is not relevant ##########
this.props.updateTokens(response.data.access,refreshToken) #<--- how i would call it if i based it off the first example
######## bunch of code that is not relevant ##########
return Promise.reject(error);
}
);
const mapDispatchToProps = dispatch => { ##<--- maping the dispatch to the 'props'
return {
updateTokens: () => dispatch(actions.authSuccess())
}
}
export default connect(mapStateToProps,mapDispatchToProps)(axiosInstance);
Ofcourse the above would give me the error that this is not defined , since its not a class . May i know the correct way of using redux in such a situation?
You can make use of store.dispatch to dispatch an action from outside of your components or action creators
import store from '/path/to/store';
axiosInstance.interceptors.response.use(
response => {
return response
},error => {
store.dispatch(actions.authSuccess(response.data.access,refreshToken));
return Promise.reject(error);
}
);

Can I memoize a setter function from a React context (useContext)

I'm trying to use useEffect to fetch data when a component is mounted as follows:
useEffect(() => {
axios.get(myUrl)
.then(response => {
setName(response.name);
setAge(response.age);
}).catch(error => {
console.log(error);
});
}, [myUrl, setName, setAge]);
setName and setAge are coming from a context as follows:
import MyContext from './context';
const MyComponent = props => {
const {
setName,
setAge
} = useContext(MyContext);
This issue is that functions, arrays, and objects all register as "changed" dependencies in a useEffect call so it ends up in an infinite loop where it's fetching that data over and over and over again. Can I memoize a function from a context so it knows to only call that effect once?
Note: The consumer of my context is several levels above this component so I'm guessing there's nothing I can do there.
I think you need to define custom context tied to something like userId.
This will provide you a stable identifier which will change only when it is necessary.
There is no way to memoize a function from a context.
That said, you can use a custom hook for useMount, either by importing it from the react-use library or by simply looking at the source code there and declaring it yourself. The following shows how to define useMount and useEffectOnce and then utilize useMount as a workaround:
const useEffectOnce = (effect) => {
useEffect(effect, []);
};
const useMount = (fn) => {
useEffectOnce(() => {
fn();
});
};
useMount(() => {
axios.get(myUrl)
.then(response => {
setName(response.name);
setAge(response.age);
}).catch(error => {
console.log(error);
});
});
With this approach, you don't need to change anything about your context declaration, context provider, or context consumer.

Update React Context using a REST Api call in a functional component

I am trying to update the context of a React App using data resulted from an API call to a REST API in the back end. The problem is that I can't synchronize the function.
I've tried this solution suggested in this blog post https://medium.com/#__davidflanagan/react-hooks-context-state-and-effects-aa899d8c8014 but it doesn't work for my case.
Here is the code for the textContext.js
import React, {useEffect, useState} from "react";
import axios from "axios";
var text = "Test";
fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
text = json;
})
const TextContext = React.createContext(text);
export const TextProvider = TextContext.Provider;
export const TextConsumer = TextContext.Consumer;
export default TextContext
And this is the functional component where I try to access the data from the context
import TextProvider, {callTextApi} from "../../../../services/textService/textContext";
function Profile()
{
const text = useContext(TextProvider);
console.log(text);
const useStyles = makeStyles(theme => ({
margin: {
margin: theme.spacing(1)
}
}));
I can see the fetch request getting the data in the network section of the browser console but the context is not getting updated.
I've tried doing this in the textContext.js.
export async function callTextApi () {
await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
And I was trying to get the data in the Profile.js using the useEffect function as so
const [text, setText] = useState(null);
useEffect(()=> {
setText (callTextApi())
},[])
It's my first time using React.context and it is pretty confusing. What am I doing wrong or missing?
You have a lot of problems here. fetching and changing should happen inside Provider by modifying the value property. useContext receives an entire Context object not only the Provider. Check the following
//Context.js
export const context = React.createContext()
Now inside your Provider
import { context } from './Context'
const MyProvider = ({children}) =>{
const [data, setData] = useState(null)
useEffect(() =>{
fetchData().then(res => setData(res.data))
},[])
const { Provider } = context
return(
<Provider value={data}>
{children}
</Provider>
)
}
Now you have a Provider that fetches some data and pass it down inside value prop. To consume it from inside a functional component use useContext like this
import { context } from './Context'
const Component = () =>{
const data = useContext(context)
return <SomeJSX />
}
Remember that Component must be under MyProvider
UPDATE
What is { children }?
Everything that goes inside a Component declaration is mapped to props.children.
const App = () =>{
return(
<Button>
Title
</Button>
)
}
const Button = props =>{
const { children } = props
return(
<button className='fancy-button'>
{ children /* Title */}
</button>
)
}
Declaring it like ({ children }) it's just a shortcut to const { children } = props. I'm using children so that you can use your Provider like this
<MyProvider>
<RestOfMyApp />
</MyProvider>
Here children is RestOfMyApp
How do I access the value of the Provider inside the Profile.js?
Using createContext. Let's assume the value property of your Provider is {foo: 'bar'}
const Component = () =>{
const content = useContext(context)
console.log(content) //{ foo : 'bar' }
}
How can you double declare a constant as you've done in the Provider?
That was a typo, I've changed to MyProvider
To access it from inside a class based component
class Component extends React.Component{
render(){
const { Consumer } = context
return(
<Consumer>
{
context => console.log(contxt) // { foo: 'bar' }
}
</Consumer>
)
}
}
First thing that I am seeing is that you are not returning the promise within your function which will lead to setting the state to undefined.
I added the return statement below:
export async function callTextApi () {
return await fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => {
return json;
})
}
Also your last then-chain could be cleaned up a bit and I am quite sure you can remove the await statement in an async function when returning a promise. It will automatically be awaited:
export async function callTextApi () {
return fetch(process.env.REACT_APP_TEXT_API)
.then(res => res.json())
.then(json => json)
}
Second step would be to have a look at your useEffect hook. You want to setText after the promise from the api call has been resolved. So you have to make the callback function of useEffect asynchronous as well.
useEffect(async ()=> {
const newText = await callTextApi();
setText (newText);
},[])
Third step, would be to look at how to properly use the context api and the useContext hook. The useContext hook takes a context as a parameter but you passed the ContextProvider as the argument.
const text = useContext(TextContext);
The context and the context-provider are two different entities in the React world. Think of the context as state and functionality that you want to share across your application (like a global state), and think about the provider as a react component that manages one context and offers this context state to it's child components.
return(
<TextContext.Provider value={/* some value */}>
{children}
</TextContext.Provider>);
This is how a return statement of a provider component would look like and I think this code is currently missing in your application.

Dispatching actions in Redux best practices?

I have a large project I am working on at work and wondering about the proper way to dispatch actions.
In my container for my component I map this function.
const mapDispatchToProps = (dispatch) => {
return {
ackMessage:(convo)=> {
chatSocketService.messageAcknowledge(convo, dispatch);
}
}
You can see I am passing the dispatch function to my Service.
I need to pass dispatch so in case the socket event fails I can dispatch an error action inside the service.
Is this ok to do? Or would it be better to always keep the dispatching in the containers. If I turned that socket service function into a promise I could do this then but then we may be adding too much logic to the dispatch function in the container?
Is passing the dispatch object around ok to do?
Edit: Here is a snippet of my socket service. On error event from my socket emit I need to dispatch an error:
const chatSocketService = {
messageAcknowledge(convo, dispatch) {
const socketConnection = getSocket();
socketConnection.emit(socketMessages.ACKNOWLEDGE_MESSAGE, {convoID:convo.convoID, msgID:convo.lastMsg.msgID },
(response)=> {
socketError(response, convo, dispatch);
});
}
}
const socketError = (response, convo, dispatch) => {
if (response.error === CHAT_SESSION_EXPIRE) {
sessionExpire(dispatch);
} else if(response.error) {
dispatch(convoError(convo.convoID, true));
}
};
const sessionExpire = (dispatch)=> {
dispatch(disconnectedMessage('Session has expired. Please log out and log back in'));
dispatch(socketDisconnected(true));
};
Then in my actions.js I have these actions:
export const CONVO_ERROR = 'CHAT_CONVO_ERROR';
export const convoError = (convoID, error) => ({
type:CONVO_ERROR,
convoID,
error
});
export const SOCKET_DISCONNECTED = 'CHAT_SOCKET_DISCONNECTED';
export const socketDisconnected = (disconnected)=> ({
type:SOCKET_DISCONNECTED,
disconnected
});
I think you should keep the dispatch function inside the container and separate out the async api call in a different file and import that function to use in this file. Also show us how you are making those async calls like chatSocketSevice using redux-thunk or redux-saga.. I feel like then I could be more helpful.

Resources