Using async await with `useSelector` in React hooks - reactjs

I have a functional component that that has an input field where the user types a question and hits enter and I send the query to the backend.
Here is the simplified version of the functional component
UserQuery.js
import {postQuery} from '../actions/postQueryAction'
const UserQuery = () => {
const [name, setName] = useState("")
function sendMessage(userQuery) {
postUserQuery(userQuery)
}
return (
<>
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
onKeyPress={sendMessage}
id="userQuery"
/>
</>
)
}
export default UserQuery
As you can see I have a callback called postQuery which actually makes the axios request and posts the user query. Here is how it looks like
postQueryAction.js
export const postQuery = (userQuery) => async dispatch => {
let userInfo = useSelector(state => state.userInfo.data)
const username = userInfo.username
const group = userInfo.group
const headers = {
'Content-Type': 'application/json;charset=UTF-8',
}
const params = {
group: group,
user: username,
data: userQuery
}
await axios.post(`/postQuestion`,params, {headers}, {
}).then(response => {
console.log("response check", response.data);
})
.catch(err => {
console.log("Error log", err);
});
}
But I get Invalid hook call error. If I remove useSelector code, then it doesn't complain and the request goes through.
I could use the useSelector in original functional component (UserQuery.js) and pass the parameters accordingly. But I want the postQuery method to only accept the userQuery parameter and figure the other information from the redux state.
What should I do?

Hooks can only be called from react components or other hooks. Instead of using the hook inside the postQuery function you could call it in the component and pass the user info to postQuery
const UserQuery = () => {
const [name, setName] = useState("");
let userInfo = useSelector((state) => state.userInfo.data);
function sendMessage(userQuery) {
postUserQuery(userQuery, userInfo);
}
...
export const postQuery = (userQuery, userInfo) => async (dispatch) => {
...

Related

Invalid Hook Call for custom hook

I have written a function for API calls. I want to reuse this function from a different page.
FetchData.js
export const FetchData = (url, query, variable) => {
const [fetchData, setFetchData] = useState([]);
useEffect(() => {
const fetchData = async () => {
const queryResult = await axios.post(
url, {
query: query,
variables: variable
}
)
const result = queryResult.data.data;
setFetchData(result.mydata)
};
fetchData();
})
return {fetchData, setFetchData}
}
Here is my main page from where I am trying to call the API using the following code
mainPage.js
import { FetchData } from './FetchData'
export const MainPage = props => {
const onClick = (event) => {
const {fetchData, setFetchData} = FetchData(url, query, variable)
console.log(fetchData)
}
}
It is returning the following error -
Uncaught 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
If you need to fetch data on response to an event, you don't need a useEffect.
const useData = (url, query, variable) => {
const [data, setData] = useState([]);
const fetchData = async () => {
const queryResult = await axios.post(url, {
query: query,
variables: variable,
});
setData(queryResult.data.data);
};
return {data, fetchData}
};
export const MainPage = (props) => {
const {data, fetchData} = useData(url, query, variable);
const onClick = (event) => {
fetchData()
};
};
Hooks can't be used inside handler functions.
Do this instead:
import { FetchData } from './FetchData'
export const MainPage = props => {
const {fetchData, setFetchData} = FetchData(url, query, variable)
const onClick = (event) => {
console.log(fetchData)
}
}

How to cancel axios call in react-redux action on change of search text?

I am using redux for doing api call and searching on basis of text. I am dispatching action onChange of text and want to cancel the alternate api calls.
Here is my code for Input -
import { useDispatch } from "react-redux";
import { searchData } from "./action";
import "./styles.css";
export default function App() {
const dispatch = useDispatch()
const handleChange = (e) => {
dispatch(searchData({ searchText : e.target.value }))
}
return (
<div className="App">
<input onChange={handleChange}/>
</div>
);
}
Code for action -
export const searchData = ({ searchText = "" }) => {
return async (dispatch) => {
dispatch(initiate());
const response = await axios.post(apiUrl, {
query: queryToCall(searchText)
});
dispatch(success(response));
};
};
I have tried this solution -
how to cancel previous axios with redux in react
Also tried to pass cancelToken as parameter in action but it doesn't seem to work.
How can I make this work?
If this is for something like an autocomplete input, the simple solution is to only keep the most recent response and ignore the results of all previous requests.
export const searchData = ({ searchText = "" }) => {
return async (dispatch, getState) => {
const startedAt = Date.now();
// Adjust this action to write this timestamp in the state,
// for example state.mostRecentSearchStartedAt (initialState 0)
dispatch(initiate(startedAt));
const response = await axios.post(apiUrl, {
query: queryToCall(searchText)
});
// Ignore response if it belongs to an outdated search request
// - a more recent search has been triggered in the meantime.
if (getState().mostRecentSearchStartedAt === startedAt) {
dispatch(success(response));
}
};
};

Get input value with react hooks to search on oMdb api

I want to do a movie search with the oMdb api using React Hooks.
The result is not as expected. I seem to break some React Hooks rule that I don't understand.
Here is the code.
HOOK TO SEARCH
The Hook inside of a store.
(If I use searchMovies('star wars') in a console.log I can see the result of star wars movies and series.)
import React, { useState, useEffect } from "react";
const useSearchMovies = (searchValue) => {
const API_KEY = "731e41f";
const URL = `http://www.omdbapi.com/?&apikey=${API_KEY}&s=${searchValue}`
// Manejador del estado
const [searchMovies, setSearchMovies] = useState([])
//Llamar y escuchar a la api
useEffect(() => {
fetch(URL)
.then(response => response.json())
.then(data => setSearchMovies(data.Search))
.catch((error) => {
console.Console.toString('Error', error)
})
}, []);
return searchMovies;
};
THE INPUT ON A SANDBOX
Here i have the input to search with a console log to see the result.
import React, { useState } from "react";
import searchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('')
const onChangeHandler = e =>{
setSearch(e.target.value)
console.log('Search result', searchMovies(search))
}
const handleInput =()=> {
console.log('valor del input', search)
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput()}>search</button>
</div>
</div>
)
}
export default Sandbox;
Issue
You are breaking the rules of hooks by conditionally calling your hook in a nested function, i.e. a callback handler.
import searchMovies from "../store/hooks/useSearchMovies";
...
const onChangeHandler = e => {
setSearch(e.target.value);
console.log('Search result', searchMovies(search)); // <-- calling hook in callback
}
Rules of Hooks
Only call hooks at the top level - Don’t call Hooks inside loops,
conditions, or nested functions.
Solution
If I understand your code and your use case you want to fetch/search only when the search button is clicked. For this I suggest a refactor of your useSearchMovies hook to instead return a search function with the appropriate parameters enclosed.
Example:
const useSearchMovies = () => {
const API_KEY = "XXXXXXX";
const searchMovies = (searchValue) => {
const URL = `https://www.omdbapi.com/?apikey=${API_KEY}&s=${searchValue}`;
return fetch(URL)
.then((response) => response.json())
.then((data) => data.Search)
.catch((error) => {
console.error("Error", error);
throw error;
});
};
return { searchMovies };
};
Usage:
import React, { useState } from "react";
import useSearchMovies from "../store/hooks/useSearchMovies";
const Sandbox = () => {
const [search, setSearch] = useState('');
const [movies, setMovies] = useState([]);
const { searchMovies } = useSearchMovies();
const onChangeHandler = e => {
setSearch(e.target.value)
};
const handleInput = async () => {
console.log('valor del input', search);
try {
const movies = await searchMovies(search);
setMovies(movies);
} catch (error) {
// handle error/set any error state/etc...
}
}
return (
<div>
<h1>Sandbox</h1>
<div>
<input type="text" value={search} onChange={onChangeHandler}/>
<button onClick={handleInput}>search</button>
</div>
<ul>
{movies.map(({ Title }) => (
<li key={Title}>{Title}</li>
))}
</ul>
</div>
);
};
export default Sandbox;

Update global state in axios post request

I have a global token variable that I want to update whenever I make an API request with axios. The problem that I am having is how to update the the token variable since the axios request is not made in a functional component, so I am not able to take advantage of React hooks when making such request.
const logInUser = async (usernameOrEmail, password) => {
//const myContext = useContext(AppContext);
//^ gives an error b/c it's a react hook
axios
.post(
`https://jellybackend.herokuapp.com/authenticate`, {
username: usernameOrEmail,
password: password,
})
.then((response) => {
//myContext.setToken(response.data.token); //update token set , error since not a functional component
console.log(response);
tokenGlobal = response.data.token
})
.catch((error) =>
console.log(error)
);
};
I am making my token a global state, so I have the hook defined in App.js, as seen below
export default function App() {
//define global variable of token
const [token, setToken] = useState("");
const userSettings = {
token: "",
setToken,
};
return (
<AppContext.Provider value={userSettings}>
...
</AppContext.Provider>
);
}
Any idea on how to update my global state variable, or how to refactor my code would be very appreciated!
What I want eventually to happen is that whenever a user logs in, the token is updated, since that is what is returned from the axios post request.
The button below is how a user logs in
function LoginScreen(props) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const myContext = useContext(AppContext);
return (
...
<Button
onPress={ () => {logInUser(email, password);} //I want to update the token here...
w="40%"
py="4"
style={styles.button}
>
A very simple and trivial refactor would be to pass callback functions to the logInUser utility.
Example:
const logInUser = async (usernameOrEmail, password, onSuccess, onFailure) => {
axios
.post(
`https://jellybackend.herokuapp.com/authenticate`, {
username: usernameOrEmail,
password: password,
})
.then((response) => {
onSuccess(response);
console.log(response);
})
.catch((error) =>
onFailure(error);
console.log(error);
);
};
...
function LoginScreen(props) {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const myContext = useContext(AppContext);
const successHandler = response => myContext.setToken(response.data.token);
const failureHandler = error => myContext.setToken(null);
return (
...
<Button
onPress={() => logInUser(email, password, successHandler, failureHandler)}
w="40%"
py="4"
style={styles.button}
>
...
</Button>
...
);
}
You could setup your axios call in a module that can then return the value that you would like to store in global state.
Your axios call doesn't have to exist within a functional component per se, but it would need to be imported/called within one for this solution.
So, you could change your axios call to be within a module function that could then be exported, say globalsapi.js, then imported to your functional component:
exports.logInUser = async () => {
const globalData = await axios
.post(
`https://jellybackend.herokuapp.com/authenticate`, {
username: usernameOrEmail,
password: password,
});
const token = await globalData.data.token;
return token;
}
Now, wherever you decide to call the setToken state update, you can just import the function and set the global token:
import { logInUser } from './globalsapi';
logInUser().then(data => {
myContext.setToken(data);
});
You could pass whatever parameters needed to the logInUser function.

How to get imported functions to set state of functional component?

I have a react class component with rather lengthy onSubmit function that I have put into another file in order to keep the code a bit tidier.
I tried to convert the class component to a functional one, replacing all of my state and setState functions with useState but now my useState state updaters are returning undefined inside the imported function. Am I able to update state using an imported function with a functional component? The function worked fine when it was imported into a class component and my state updater was setState();
//Imported function in utils.js
export const loginUser = async function (email, password) {
try {
const login = await axios.post('http://localhost:5000/api/v1/auth/login', {
email,
password
});
const options = {
headers: {
Authorization: `Bearer ${login.data.token}`
}
};
const getUser = await axios.get(
'http://localhost:5000/api/v1/auth/me',
options
);
const user = getUser.data.data;
setAuthenticated(true);
setUser(getUser.data.data);
setEmail('');
setPassword('');
localStorage.setItem('user', JSON.stringify(user));
console.log(localStorage.getItem('user'));
} catch (err) {
console.log(err);
}
};
// Functional component with imported function
import React, { useState } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import { Login } from './Login';
const { loginUser } = require('../utils/utils');
export const Splash = () => {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
const [authenticated, setAuthenticated] = useState(false);
const [msg, setMsg] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const _handleEmail = (e) => {
setEmail(e.target.value);
};
const _handlePass = (e) => {
setPassword(e.target.value);
};
const _handleSubmit = async (e) => {
e.preventDefault();
loginUser(email, password);
if (user) {
console.log(user);
this.props.onHandleUser(user);
}
};
return (
<div className='splashStyle'>
{!authenticated && (
<Login
handleEmail={_handleEmail}
handlePass={_handlePass}
handleSubmit={_handleSubmit}
isAuthenticated={authenticated}
/>
)}
</div>
);
};d
EDIT: My issue that setAuthenticated, setUser, setEmail, and setPassword are coming undefined in utils.js
Thanks!
One way of achieving that would be passing all the set methods as a paramters to loginUser function.
But a better way of doing this will be like:
create two separate files
1 for login api call like :
login.js
function login(email, password){
const login = await axios.post('http://localhost:5000/api/v1/auth/login', {
email,
password
});
return login.data;
}
another for getting data
getProfile.js
function getProfile(token){
const options = {
headers: {
Authorization: `Bearer ${token}`
}
};
const getUser = await axios.get(
'http://localhost:5000/api/v1/auth/me',
options
);
return getUser.data;
}
Now do you setting state stuff in actuall component submit call function like
const _handleSubmit = async (e) => {
e.preventDefault();
const token = await login(email, password);
const user = await getProfile(token);
if (user) {
console.log(user);
props.onHandleUser(user);
setAuthenticated(true);
setUser(getUser.data.data);
setEmail('');
setPassword('');
localStorage.setItem('user', JSON.stringify(user));
console.log(localStorage.getItem('user'));
}
};
You need to pass the setAuthenticated function to the loginUser function before calling it in that.
return an onSubmiHandler function from your login user hook.
const doLogin = (email , password) => {
/// your code here
}
return {doLogin}
then use the doLogin function inside your main component
//inside functional component
const {doLogin} = loginUser();
onSubmit = () => doLogin(email, password)
for more you can see how to use custom hooks from here
https://reactjs.org/docs/hooks-custom.html
To start loginUser can't know about the setState you insert there try to pass it as arguments and it will fix it 😁
another problem I see is that you use the this keyword and in the functional component you use the just props.
and just for you to know don't pass null as the initial value pass an empty string, number, etc..
Update
this is how you also pass a setState as argument
loginUser((e)=>setEmail(e))

Resources