I have created an input field with a search term which creates a request to a backend API. To summarise, two issues:
It fetches data from my API, but it fetches ALL roles, not just ones filtered by the term.
It does not commit to the redux store.
Please see my app, it contains simply:
This is my frontend component, which is making an action dispatch based on a search term.
export function SearchBarTrialRedux(props) {
const [isExpanded, setExpanded] = useState(false);
const [parentRef, isClickedOutside ] = useClickOutside();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setLoading] = useState(false);
const [jobPostings, setjobPostings] = useState([]);
const [noRoles, setNoRoles] = useState(false)
const isEmpty = !jobPostings || jobPostings.length === 0;
const expandedContainer = () => {
setExpanded(true);
}
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoRoles(false);
if (inputRef.current) inputRef.current.value = "";
};
useEffect(()=> {
if(isClickedOutside)
collapseContainer();
}, [isClickedOutside])
const [term, setTerm] = useState("")
const dispatch = useDispatch();
const changeHandler = (e) => {
e.preventDefault();
fetchAsyncRoles(dispatch, {term});
}
return (
<SearchBarContainer animate = {isExpanded ? "expanded" : "collapsed"}
variants={containerVariants} transition={containerTransition} ref={parentRef}>
<SearchInputContainer>
<SearchIconSpan>
<SearchIcon/>
</SearchIconSpan>
<form onSubmit={changeHandler}>
<SearchInput placeholder = "Search for Roles"
onFocus={expandedContainer}
ref={inputRef}
value={term}
onChange={(e)=> setTerm(e.target.value)}
/>
</form>
</SearchBarContainer>
And my jobsearchSlice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { publicRequest } from "../requestMethods";
export const fetchAsyncRoles = async (dispatch, term) => {
dispatch(searchStart());
try {
const res = await publicRequest.get(`http://localhost:5000/api/role/titlerole?title=${term}`);
dispatch(searchSuccess(res.data));
console.log(res.data)
} catch (err) {
dispatch(searchFailure());
}
};
const jobsearchSlice = createSlice({
name: "jobsearchSlice",
initialState: {
isFetching: false,
roles: [],
error: false,
},
reducers: {
searchStart: (state) => {
state.isFetching = true;
},
searchSuccess: (state, action) => {
state.isFetching = false;
state.roles = action.payload;
},
searchFailure: (state) => {
state.isFetching = false;
state.error = true;
},
},
});
export const { searchStart, searchSuccess, searchFailure } = jobsearchSlice.actions;
export default jobsearchSlice.reducer;
As stated, it does create and fetch this data. This does commit it to the store under the roles key, which is great! That's what I want, however it is not filtering. E.g If we look at a role specifically like Data Scientist:
https://gyazo.com/ca4c2b142771edd060a7563b4200adf8
I should be getting just 1 key, Data Scientist.
Looking at the backend of the console.log(res), I can see that it appears my term isn't properly coming through and filtering my roles :
responseURL: "http://localhost:5000/api/role/titlerole?title=[object%20Object]"
But if I log the term, it does come through exactly as input.
What's wrong, what am I doing and how should I solve this term flowing through to filter my req?
I can confirm that if I do this on postman it works...
https://gyazo.com/10f2946c1a3807370b4792c06292b557
Related
I have two functions, one where I am able to send an order that updates the users balance amongst some other things, and another which retrieves the users balance for the user to see. Before any orders happen I still need to retrieve the balance for the user to see, thus I have broken my getBalance func from MarketLongFunc.
Using redux-toolkit and redux-thunk I have an ordersSlice.js that looks like this:
export const MarketLongFunc = createAsyncThunk(
"/order/marketlong",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
value: value,
token: token,
};
let url = `http://localhost:3001/api/orders/marketlong`;
const response = await axios.post(url, newObj);
//getBalance()
return;
}
);
export const getBalance = createAsyncThunk(
"/order/getBalance",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
token: token,
};
let url = `http://localhost:3001/api/orders/getBalance`;
const response = await axios.post(url, newObj);
return response.data;
}
);
const initialState = {
value: null,
error: null,
balance: null,
status: "idle",
orderStatus: "idle",
};
export const ordersSlice = createSlice({
name: "orders",
initialState,
reducers: {
reset: (state) => initialState,
resetStatus: (state) => {
state.orderStatus = "idle";
},
},
extraReducers(builder) {
builder
.addCase(MarketLongFunc.pending, (state, action) => {
state.orderStatus = "loading";
})
.addCase(MarketLongFunc.fulfilled, (state, action) => {
state.orderStatus = "success";
// getBalance();
// state.balance = action.payload;
})
.addCase(MarketLongFunc.rejected, (state, action) => {
state.orderStatus = "failed";
state.error = action.error.message;
})
.addCase(getBalance.pending, (state, action) => {
state.status = "loading";
})
.addCase(getBalance.fulfilled, (state, action) => {
// state.status = "success";
state.balance = action.payload;
state.status = "success";
})
.addCase(getBalance.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
},
});
export const { reset } = ordersSlice.actions;
export default ordersSlice.reducer;
Now in my next component the useEffect will call if there is no balance yet and the user is logged in. The way in which I was trying to solve my issue was to use the state.orderStatus = "success" under MarketLongFunc.fulfilled, this way hypothetically I can dispatch getbalance under the useEffect if a MarketLong is placed and then change the status with reset like the following:
export const Orderform = () => {
const user = useSelector((state) => state.auth.user);
const balance = useSelector((state) => state.orders.balance);
const status = useSelector((state) => state.orders.orderStatus);
const dispatch = useDispatch();
useEffect(() => {
if (!balance && user) {
dispatch(getBalance());
}
if (status == "success") {
dispatch(getBalance());
dispatch(resetStatus());
}
}, [balance]);
if (user) {
return (
<div>
<h1>
cash balance: ${balance ? Math.round(balance.balance) : "error"}
</h1>
<MarketLong />
</div>
);
}
return (
<div>
Login
</div>
);
};
The above code does not work currently as when I console.log(status) on refresh is is idle and when I use marketLong it is loading but it never makes it to fulfilled so still the only way to update the balance that is displayed after an order is to refresh the page. I want to update the displayed balance without refreshing the page as refreshing the page will have to make two other API calls on top of the getBalance. I have left some comments in where I tried things like just putting the getBalance function inside the MarketLongFunc in the ordersSlice, I also tried returning it etc but that did nothing and I figured fixing this issue in the useEffect with the status' would be the best way to fix this but I am open to hearing other solutions besides creating redundant code where I just basically type out the getBalance func inside marketLongFunc.
Another way that almost works is just adding dispatch(getBalance()) after dispatch(MarketLongFunc(longItem)); in my MarketLong react component like the following:
const addNewLong = async (e) => {
e.preventDefault();
const longItem = {
userInput: req.ticker,
quotePrice: req.quotePrice,
quantity: Item.quantity,
};
dispatch(MarketLongFunc(longItem));
dispatch(getBalance());
};
The problem with this is the first order never gets updated but after that it updates incorrectly as the balance will be off by one buy order. I imagine this is due to getBalance gettting called before MarketLongFunc but without setting a manual setTimeout func which seems like a clunky solution, I am not sure how to fix that with redux, you would think something like : if (dispatch(MarketLongFunc(longItem))) {dispatch(getBalance())}, but maybe this way needs to be changed in the ordersSlice (which I had tried and was not able to get it to work).
There are many ways to solve this problem - I will describe an approximate solution:
export const MarketLongFunc = createAsyncThunk(
"/order/marketlong",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
value: value,
token: token,
};
let url = `http://localhost:3001/api/orders/marketlong`;
const response = await axios.post(url, newObj);
//getBalance()
return;
}
);
export const getBalance = createAsyncThunk(
"/order/getBalance",
async (value, thunkAPI) => {
const token = thunkAPI.getState().auth.user.token;
const newObj = {
token: token,
};
let url = `http://localhost:3001/api/orders/getBalance`;
const response = await axios.post(url, newObj);
return response.data;
}
);
const initialState = {
value: null,
error: null,
balance: null,
status: "idle",
orderStatus: "idle",
balanceNeedsToBeUpdated: true // <--- HERE
};
export const ordersSlice = createSlice({
name: "orders",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers(builder) {
builder
.addCase(MarketLongFunc.pending, (state, action) => {
state.orderStatus = "loading";
})
.addCase(MarketLongFunc.fulfilled, (state, action) => {
state.orderStatus = "idle";
state.balanceNeedsToBeUpdated = true; // < ----- HERE
// getBalance();
// state.balance = action.payload;
})
.addCase(MarketLongFunc.rejected, (state, action) => {
state.orderStatus = "failed";
state.error = action.error.message;
})
.addCase(getBalance.pending, (state, action) => {
state.status = "loading";
})
.addCase(getBalance.fulfilled, (state, action) => {
// state.status = "success";
state.balance = action.payload;
state.status = "idle";
state.balanceNeedsToBeUpdated = false; // <---- HERE
})
.addCase(getBalance.rejected, (state, action) => {
state.status = "failed";
state.error = action.error.message;
});
},
});
export const { reset } = ordersSlice.actions;
export default ordersSlice.reducer;
export const Orderform = () => {
const user = useSelector((state) => state.auth.user);
const balance = useSelector((state) => state.orders.balance);
const status = useSelector((state) => state.orders.status);
const orderStatus = useSelector((state) => state.orders.orderStatus);
const balanceNeedsToBeUpdated = useSelector((state) => state.orders.balanceNeedsToBeUpdated);
const dispatch = useDispatch();
useEffect(() => {
if (user && balanceNeedsToBeUpdated) { //< ----- HERE
dispatch(getBalance());
}
}, [user, balanceNeedsToBeUpdated]); // < ---- HERE
if (user) {
if (status == 'loading' || orderStatus == 'loading') {
return <div>loading</div>;
}
return (
<div>
<h1>
cash balance: ${balance ? Math.round(balance.balance) : "error"}
</h1>
<MarketLong />
</div>
);
}
return (
<div>
Login
</div>
);
};
//....
const addNewLong = async (e) => {
e.preventDefault();
const longItem = {
userInput: req.ticker,
quotePrice: req.quotePrice,
quantity: Item.quantity,
};
dispatch(MarketLongFunc(longItem)); // < --- HERE
};
Bit of a noob to redux but this appears to be quite a difficult question! I hope someone may be able to help me :)
I have build a page where you can input a search for different types of jobs. From this, it will make a get request to my DB and get all the info on this job. As this page is multi-levelled, I want to use redux to dispatch and pass the state throughout. This will help me pass my data on the job, e.g Data Analyst, through to each component so it can use the data and populate fields.
However, this was how my input field was originally setup:
export function SearchBarComp(props) {
const [isExpanded, setExpanded] = useState(false);
const [parentRef, isClickedOutside ] = useClickOutside();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setLoading] = useState(false);
const [jobPostings, setjobPostings] = useState([]);
const [noRoles, setNoRoles] = useState(false)
const isEmpty = !jobPostings || jobPostings.length === 0;
const changeHandler = (e) => {
//prevents defaulting, autocomplete
e.preventDefault();
if(e.target.value.trim() === '') setNoRoles(false);
setSearchQuery(e.target.value);
}
const expandedContainer = () => {
setExpanded(true);
}
//LINK THE BACKEND!
const prepareSearchQuery = (query) => {
//const url = `http://localhost:5000/api/role/title?title=${query}`;
const url = `http://localhost:5000/api/role/titlerole?title=${query}`;
//replaces bad query in the url
return encodeURI(url);
}
const searchRolePosition = async () => {
if(!searchQuery || searchQuery.trim() === "")
return;
setLoading(true);
setNoRoles(false);
const URL = prepareSearchQuery(searchQuery);
const response = await axios.get(URL).catch((err) => {
console.log(err);
});
if(response) {
console.log("Response", response.data);
if(response.data && response.data === 0)
setNoRoles(true);
setjobPostings(response.data);
}
setLoading(false);
}
useDebounce(searchQuery, 500, searchRolePosition)
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoRoles(false);
if (inputRef.current) inputRef.current.value = "";
};
// console.log("Value", searchQuery)
useEffect(()=> {
if(isClickedOutside)
collapseContainer();
}, [isClickedOutside])
return (
<SearchBarContainer animate = {isExpanded ? "expanded" : "collapsed"}
variants={containerVariants} transition={containerTransition} ref={parentRef}>
<SearchInputContainer>
<SearchIconSpan>
<SearchIcon/>
</SearchIconSpan>
<SearchInput placeholder = "Search for Roles"
onFocus={expandedContainer}
ref={inputRef}
value={searchQuery}
onChange={changeHandler}
/>
<AnimatePresence>
{isExpanded && (<CloseIconSpan key="close-icon"
inital={{opacity:0, rotate: 0}}
animate={{opacity:1, rotate: 180}}
exit={{opacity:0, rotate: 0}}
transition={{duration: 0.2}}
onClick={collapseContainer}>
<CloseIcon/>
</CloseIconSpan>
)}
</AnimatePresence>
</SearchInputContainer>
{isExpanded && <LineSeperator/>}
{isExpanded && <SearchContent>
{!isLoading && isEmpty && !noRoles && (
<Typography color="gray" display="flex" flex="0.2" alignSelf="center" justifySelf="center">
Start typing to search
</Typography>
)}
{!isLoading && !isEmpty && <>
{jobPostings.map((searchRolePosition) => (
<JobSection
title={searchRolePosition.title}
//will need to do something like ----
//people = {searchRolePosition.title && searchRolePosition.title.average}
// future implementations
/>
))}
</>}
</SearchContent>}
</SearchBarContainer>
)
}
As you can see, the main thing is the 'query' this creates a backend request to my titlerole, such as getting the data on Data Analyst. This all works in my frontend right now, but I can't pass that data down to the next component etc
So i'm looking to use redux.
I've created the following slice:
import { createSlice } from "#reduxjs/toolkit";
const jobSearchSlice = createSlice({
name: "jobsearch",
initialState: {
currentRole: null,
isFetching: false,
error: false,
},
reducers: {
jobsearchStart: (state) => {
state.isFetching = true;
},
jobsearchSuccess: (state, action) => {
state.isFetching = false;
state.currentRole = action.payload;
},
jobsearchFailure: (state) => {
state.isFetching = false;
state.error = true;
},
},
});
export const { jobsearchStart, jobsearchSuccess, jobsearchFailure } = jobSearchSlice.actions;
export default jobSearchSlice.reducer;
With this, I'm also using the following apiCalls.js file:
import { jobsearchStart, jobsearchSuccess, jobsearchFailure } from "./jobsearchSlice";
import { publicRequest } from "../requestMethods";
export const roleSearchQuery = async (dispatch, jobsearch) => {
dispatch(jobsearchStart());
try{
const res = await publicRequest.get("`http://localhost:5000/api/role/titlerole?title=${query}`", jobsearch);
dispatch(jobsearchSuccess(res.data))
}catch(err){
dispatch(jobsearchFailure());
}
};
My question is as a Redux noob, how do I implement this query functionality into a redux API request? What's the way to do this properly as I begin to tranisition this to an app which uses standard state management!
My code
UserEdit.jsx
import useForm from "../../utils/useForm";
import LoadingBtn from "../../utils/loadingButton";
import { getUser } from "../../store/users/userActions";
const UserEdit = () => {
//declare form data and scmena
const formInput = {
name: "",
};
const schema = {
name: Joi.string().required().min(3).max(191).label("Name"),
};
//dispatch on first mount
const dispatch = useDispatch();
const params = useParams();
useEffect(() => {
setErrors({});
dispatch(getUser(params.id));
}, []);
const {
handleChange,
handleSubmit,
formData,
setFormData,
errors,
setErrors,
} = useForm(formInput, doSubmit, schema);
function doSubmit() {
console.log("handle Submit", formData);
}
const isSubmitting = useSelector((state) => state.users.isSubmitting);
const user = useSelector((state) => state.users.user);
useEffect(() => {
console.log("useEffect user >>", user);
setFormData({
...formData,
name: user.name,
});
}, [user]);
return (
<input
className={`form-control ${
errors["name"]
? "is-invalid"
: ""
}`}
type="text"
id="name"
name="name"
required=""
placeholder="Enter your name"
onChange={handleChange}
value={formData.name}
/>
);
};
export default UserEdit;
UseForm.jsx
const useForm = (formInput, callback, schema = {}) => {
const [formData, setFormData] = useState(formInput);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setFormData({
...formData,
[name]: value,
});
};
const handleSubmit = (e) => {
e.preventDefault();
//handle error
setErrors(validate(formData));
setIsSubmitting(true);
};
const validate = (formData) => {
const { error } = Joi.validate(formData, schema, {
abortEarly: false,
});
if (!error) return {};
const validataionErrors = {};
for (let item of error.details) {
validataionErrors[item.path[0]] = item.message;
}
return validataionErrors;
};
useEffect(() => {
//check if there are any errors
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
setIsSubmitting(true);
}
}, [errors]);
return {
handleChange,
handleSubmit,
formData,
setFormData,
errors,
setErrors,
};
};
export default useForm;
I googled about the error and it mentioned that state need to be initialized at first with the field. However, I have already defined initial state as
const formInput = {
name: "",
};
I could not find how could I fix this, I am open to restructure the useForm hooks if that is the one which causing trouble.
currently, if I uncomment the following line on userEdit.jsx, the warning will be gone, but also the edit form becomes empty as well
setFormData({
...formData,
name: user.name,
});
I have two components similar like below:
const Login = props => {
let loading;
const dispatch = useDispatch();
const [notification, setNotification] = React.useState('');
const [hasNotification, setHasNotification] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const {status, message} = useSelector(state => state.LoginReducer);
const { register, handleSubmit, formState, errors } = useForm({
mode: "onChange"
});
const onSubmit = data => {
setIsLoading(true);
dispatch(loginStart(data));
};
React.useEffect(() => {
setIsLoading(false);
if (status === 422) {
setNotification(message);
setHasNotification('ERROR');
return;
}
if (status === 200) {
setNotification(message);
setHasNotification('SUCCESS');
}
}, [status, message]);
React.useEffect(() => {
console.log('componentDidMount');
return () => {
setNotification('');
setHasNotification('');
};
}, []);
return (
<AuthLayout title={'Login'} header={'Welcome back, Sign in'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Login;
I also have another component with similar functionality as above
const Signup = props => {
let loading;
const dispatch = useDispatch();
const [notification, setNotification] = React.useState('');
const [hasNotification, setHasNotification] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const {status, message} = useSelector(state => state.SignupReducer);
const { register, handleSubmit, formState, errors } = useForm({
mode: "onChange"
});
const onSubmit = data => {
setIsLoading(true);
dispatch(signupStart(data));
};
React.useEffect(() => {
setIsLoading(false);
if (status === 422) {
setNotification(message);
setHasNotification('ERROR');
return;
}
if (status === 200) {
setNotification(message);
setHasNotification('SUCCESS');
}
}, [status, message]);
React.useEffect(() => {
console.log('componentDidMount');
return () => {
setNotification('');
setHasNotification('');
};
}, []);
return (
<AuthLayout title={'Signup'} header={'Discover a new way to do amazing work'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Signup;
I read about custom hooks but just curious how I can move the state and logic to a separate custom hook function since they have similar structure and functionalities.
What will the custom hook look like?
You can declare all your state/hooks logic in a function and export it to your component:
Example: For your login component you can extract your logic to a file, let's call it useLogin.js
useLogin.js:
export default () => {
const [notification, setNotification] = React.useState('');
const [hasNotification, setHasNotification] = React.useState('');
const [isLoading, setIsLoading] = React.useState(false);
const { register, handleSubmit, formState, errors } = useForm({
mode: "onChange"
});
React.useEffect(() => {
setIsLoading(false);
if (status === 422) {
setNotification(message);
setHasNotification('ERROR');
return;
}
if (status === 200) {
setNotification(message);
setHasNotification('SUCCESS');
}
}, [status, message]);
React.useEffect(() => {
console.log('componentDidMount');
return () => {
setNotification('');
setHasNotification('');
};
}, []);
return [notification, hasNotification, setIsLoading]; //return all variable and functions that you need in your component
}
And in Login you should import your function and use it
import useLogin from './useLogin'; // first import useLogin function
const Login = props => {
let loading;
const dispatch = useDispatch();
const {status, message} = useSelector(state => state.LoginReducer);
const [notification, hasNotification, setIsLoading] = useLogin(); // call useLogin and get notification and hasNotification objects
const onSubmit = data => {
setIsLoading(true);
dispatch(loginStart(data));
};
return (
<AuthLayout title={'Login'} header={'Welcome back, Sign in'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Login;
Same thing to Signup component
import useLogin from './useLogin';
const Signup = props => {
let loading;
const dispatch = useDispatch();
const {status, message} = useSelector(state => state.SignupReducer);
const [notification, hasNotification, setIsLoading] = useLogin();
const onSubmit = data => {
setIsLoading(true);
dispatch(signupStart(data));
};
return (
<AuthLayout title={'Signup'} header={'Discover a new way to do amazing work'} hasNotification={hasNotification} notification={notification}>
</AuthLayout>
)
}
export default Signup;
Hope the idea was clear;
You can create a new component with the same code, the difference is in the title and header from AuthLayout
<AuthLayout title={props.title} header={props.header} hasNotification={hasNotification} notification={notification}></AuthLayout>
Login
const Login = props => {
return (
<newComponent title={'Login'} header={'Welcome back, Sign in'} />
)
}
export default Login;
SignUp
const SignUp = props => {
return (
<newComponent title={'SignUp'} header={'Discover a new way to do amazing work'} />
)
}
export default SignUp;
I called newComponent, the component that you will create
As the title said, what is the correct way of using custom hook to handle onClick Event?
This codesandbox application will display a new quote on the screen when user clicks the search button.
function App() {
const [{ data, isLoading, isError }, doFetch] = useDataApi(
"https://api.quotable.io/random"
);
return (
<Fragment>
<button disabled={isLoading} onClick={doFetch}>
Search
</button>
{isError && <div>Something went wrong ...</div>}
{isLoading ? <div>Loading ...</div> : <div>{data.content}</div>}
</Fragment>
);
}
I created a custom hook called useDataApi() which would fetch a new quote from an API. In order to update the quote when the user clicks the button, inside the useDataApi(), I created a handleClick() which will change the value of a click value to trigger re-render. And this handleClick() function will be return back to App()
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [click, setClick] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const handleClick = () => {
setClick(!click);
};
useEffect(() => {
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
fetchData();
}, [initialUrl, click]);
return [{ data, isLoading, isError }, handleClick];
};
This is working, however, I don't feel this is the correct solution.
I also tried moving the fetchData() out of useEffect and return the fetchData(), and it works too. But according to the React Doc, it says it is recommended to move functions inside the useEffect.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const fetchData = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
useEffect(() => {
fetchData();
}, []);
return [{ data, isLoading, isError }, fetchData];
};
In addition, for creating these kinds of application, is the way that I am using is fine or there is another correct solution such as not using any useEffects or not create any custom Hook?
Thanks
Not sure if this is correct, but here is my solution.
const useDataApi = initialUrl => {
const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const doFetch = async () => {
setIsError(false);
setIsLoading(true);
try {
const result = await axios(initialUrl);
setData(result.data);
} catch (error) {
setIsError(true);
}
setIsLoading(false);
};
return [{ data, isLoading, isError }, doFetch];
};
Btw, don't mutate state directly.
const handleClick = () => {
setClick(!click); // don't do this
setClick(prev => !prev); // use this
};
Your implementation is fine. We are also using something similar. Hope you find it useful.
function useApi(promiseFunction, deps, shouldRun=true){
// promisFunction returns promise
const [loading, setLoading] = useState(false)
const [data, setData] = useState(false)
const [error, setError] = useState(false)
const dependencies: any[] = useMemo(()=>{
return [...dependencyArray, shouldRun]
},[...dependencyArray, shouldRun])
const reload = () => {
async function call() {
try {
setError(null)
setLoading(true)
const res = await promiseFunction();
}
catch (error) {
setError(error)
}
finally {
setLoading(false)
}
}
call();
}
useEffect(() => {
if(!shouldRun) return
setResult(null) //no stale data
reload()
}, dependencies)
return {loading, error, data, reload, setState: setData}
}
Below code will provide some idea about how to use it.
function getUsersList(){
return fetch('/users')
}
function getUserDetail(id){
return fetch(`/user/${id}`)
}
const {loading, error, data } = useApi(getUsersList, [], true)
const {loading: userLoading, error: userError, data: userData}
= useApi(()=>getUserDetail(id), [id], true)