I am struggling with an issue with the custom fetch hook.Simply i am trying to test my fetch hook if the data already fetched the hook needs to get data from cache instead of api.
The test case fails and looks like caching mechanism not working, but if i try on the browser with manual prop change caching mechanism works properly.
import { render, waitFor } from "#testing-library/react";
const renderList = (filterParams = testFilterParamsList[0]) =>
render(<List filterParams={filterParams} />);
it("should re-render without fetch", async () => {
const { rerender } = renderList(testFilterParamsList[0]);
rerender(<List filterParams={testFilterParamsList[1]} />);
expect(window.fetch).toHaveBeenCalledTimes(1);
});
// useFetch.js
import {useEffect, useReducer} from "react";
const cache = {};
const FETCH_REQUEST = "FETCH_REQUEST";
const FETCH_SUCCESS = "FETCH_SUCCESS";
const FETCH_ERROR = "FETCH_SUCCESS";
const INITIAL_STATE = {
isPending: false,
error: null,
data: [],
};
const useFetch = ({url, filterOptions}) => {
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case FETCH_REQUEST:return {...INITIAL_STATE, isPending: true};
case FETCH_SUCCESS: return {...INITIAL_STATE, isPending: false, data: action.payload};
case FETCH_ERROR: return {...INITIAL_STATE, isPending: false, error: action.payload};
default: return state;
}
}, INITIAL_STATE);
useEffect(() => {
const fetchData = async () => {
dispatch({type: FETCH_REQUEST});
if (cache[url]) {
const data = cache[url];
dispatch({type: FETCH_SUCCESS, payload: data});
} else {
try {
const response = await window.fetch(url);
let data = await response.json();
cache[url] = data
dispatch({type: FETCH_SUCCESS, payload: data});
} catch (err) {
dispatch({type: FETCH_ERROR, payload: err});
}
}
};
fetchData();
}, [filterOptions, url]);
return state;
};
export default useFetch;
// List.js
import useFetch from "../hooks/useFetch";
export const RocketsList = ({ filterParams }) => {
const { isPending, error, data } = useFetch({
url: "https://api.spacexdata.com/v3/launches/past",
name:filterParams.name,
});
return (
<div>
Doesn't matter
</div>
);
};
Related
i need help with redux toolkit, i'm using redux to fetch one array of objects but this not happen bellow is my redux code and get api.
GET API
export const getProductsApi = async () => {
const response = await fetch(
`${gateway.url}/products`,
{
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
}
);
const data = await response.json();
return data;
}
MY REDUX ACTION
import {
getProductsPending,
getProductsSuccess,
getProductsFailed,
} from "../../slices/get-products";
import { getProductsApi } from "../../../api/get-products";
export const getProducts = () => async (dispatch: any) => {
dispatch(getProductsPending());
try {
const response = await getProductsApi();
const data = response.data;
console.log('products redux here', data);
dispatch(getProductsSuccess(data));
} catch (error) {
dispatch(getProductsFailed(error));
}
};
REDUX SLICE
const getProductsSlice = createSlice({
name: "getProducts",
initialState,
reducers: {
getProductsPending: (state) => {
state.isLoading = true;
state.error = "";
},
getProductsSuccess: (state, action: PayloadAction<Product[]>) => {
state.isLoading = false;
state.products = action.payload;
state.error = "";
},
getProductsFailed: (state, action) => {
state.isLoading = false;
state.error = action.payload.error;
state.products = [];
},
},
});
const { reducer, actions } = getProductsSlice;
export const { getProductsPending, getProductsSuccess, getProductsFailed } =
actions;
export default reducer;
REDUX STORE
import { configureStore } from "#reduxjs/toolkit";
import getProductsReducer from "../redux/slices/get-products";
const store = configureStore({
reducer: {
products: getProductsReducer,
},
});
store.subscribe(() => console.log(store.getState()));
export default store;
in my browser console when is to appear the data, data appear so
whit my products array empty, and i don't know where is my error because anothers redux i made, i make like this and work correctly.
I appreciate every help to solve my question.
using redux tookit
I am using Redux to create a search bar and React to render out JSON data into a card. I am using thunk middleware and pass an initial state to React. When I make a search, I can successfully fetch data from my API, and a card is created. However, the data in the card is empty, which suggests the API data is not being stored in the properly.
Store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import {composeWithDevTools} from 'redux-devtools-extension';
import searchReducer from './reducers/searchreducers';
const store = createStore(searchReducer, composeWithDevTools(applyMiddleware(thunk)));
export default store;
searchreducers.js
const initState = {
brand: "",
result: "",
loading: false
};
const searchReducer = (state=initState, action) => {
switch (action.type) {
case "LOADING":
return {
...state,
brand: action.payload,
loading: true
};
case "LOAD_RESULT":
return {
...state,
brand: action.payload,
loading: false,
error: false
};
case "SET_ERROR":
return {
...state,
error: action.payload,
loading: false,
};
default:
return state;
}
}
export default searchReducer;
actions.js
import axios from 'axios';
export const loading = (brand) => {
return {
type: "LOADING",
payload: brand
}
}
export const loadResult = (result) => {
return {
type: "LOAD_RESULT",
payload: result
}
}
export const getResult = (searchTerm) => {
return async (dispatch) => {
dispatch(loading(searchTerm))
try {
const { data } = await axios.get(
`http://makeup-api.herokuapp.com/api/v1/products.json?brand=${searchTerm}`
)
console.log(data[0]) //returns data correctly
dispatch(loadResult(data[0]))
} catch (err) {
console.error(err)
dispatch({
type: "SET_ERROR",
payload: err
})
}
}
}
Result.js
render card
import React from 'react';
const Result = (result) => {
return(
<div className="card">
<img src={result.image_link} alt={result.name}/>
<h1>{result.brand}</h1>
<h3>{result.name}</h3>
<p>{result.description}</p>
</div>
)
}
export default Result
page.js
const Search = () => {
const result = useSelector(state => state.result)
const loading = useSelector(state => state.loading)
const dispatch = useDispatch();
useEffect(console.log(result)) //returns empty
const renderResult = () => {
return loading ? <p>Loading...</p> : <Result result={result}/>
}
const search = searchTerm => dispatch(getResult(searchTerm))
return(
<>
<div id="search">
<SearchForm getResult={search}/>
<h1>Result</h1>
{renderResult()}
</div>
</>
)
};
export default Search;
My first theory is that it's because the data is in json form but when I JSON.parse(data), it some up as [object Object] is not a valid JSON.
Also changed const Result = (result) => {...} to const Result = ({result}) => {..} but keys come up as undefined (may also be due to data format).
I'm trying to check if my store is onboarded or not. for that, I'm making an API call through the redux to check it in the BE and if it's true I'll redirect it to the dashboard. I'm able to get the data successfully from BE, and on success checkIsStoreOnboardedSuccess() is called but in the reducer, the state is not updated with the CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS state in the reducer.
action.js
import * as actionTypes from './index';
import API from '../../api';
export const clearCheckIsStoreOnboarded = () => {
return {
type: actionTypes.CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING,
};
};
export const checkIsStoreOnboarded = (payload) => {
return (dispatch) => {
dispatch(checkIsStoreOnboardedInitiate());
API.getAccountSettings(payload)
.then((response) => {
checkIsStoreOnboardedSuccess(response.data);
})
.catch((err) => {
checkIsStoreOnboardedFailure(err);
});
};
};
const checkIsStoreOnboardedInitiate = () => {
return {
type: actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START,
};
};
const checkIsStoreOnboardedSuccess = (data) => {
return {
type: actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS,
data: data,
};
};
const checkIsStoreOnboardedFailure = (err) => {
return {
type: actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL,
data: err,
};
};
reducer.js
import * as actionTypes from '../actions';
const initialState = {
isLoading: true,
isError: false,
isDone: false,
data: [],
error: null,
};
const clearCheckIsStoreOnboarded = () => {
return initialState;
};
const checkIsStoreOnboardedStart = (state) => {
return { ...state, isLoading: true, error: null, isError: false };
};
const checkIsStoreOnboardedSuccess = (state, action) => {
return { ...state, data: action.data, isDone: true, isLoading: false };
};
const checkIsStoreOnboardedFailure = (state, action) => {
return { ...state, error: action.data, isLoading: false, isError: true };
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING:
return clearCheckIsStoreOnboarded();
case actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START:
return checkIsStoreOnboardedStart(state);
case actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS:
return checkIsStoreOnboardedSuccess(state, action);
case actionTypes.CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL:
return checkIsStoreOnboardedFailure(state, action);
default:
return state;
}
};
export default reducer;
actionTypes.js
export const CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING = 'CLEAR_CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING';
export const CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START = 'CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_START';
export const CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS = 'CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_SUCCESS';
export const CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL = 'CHECK_IS_STORE_ONBOARDED_FOR_ONBOARDING_FAIL';
onboard.js
import React, { useState, useEffect } from 'react';
import { withCookies } from 'react-cookie';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import Crew from './Crew';
import Service from './Services';
import Address from './Address';
import { useStyles } from './css/index.css';
import Header from './header';
import Stepper from './stepper';
import { getStoreID } from '../../../utils';
import {
clearCheckIsStoreOnboarded,
checkIsStoreOnboarded,
} from '../../../store/actions/check-is-store-onboarded-for-onboarding'
import Loader from '../../../components/CircularProgressLoader';
const OnboardScreen = ({
cookies,
clearCheckIsStoreOnboarded,
checkIsStoreOnboarded,
checkIsStoreOnboardedData,
}) => {
const [step, setStep] = useState(0);
// eslint-disable-next-line no-unused-vars
const [width, isDesktop] = useWindowWitdh();
const classes = useStyles(isDesktop);
const store_id = getStoreID(cookies);
useEffect(() => {
checkIsStoreOnboarded({
store_id,
});
}, []);
useEffect(() => () => clearCheckIsStoreOnboarded(), []);
if(checkIsStoreOnboarded.isDone){
<Redirect to='/dashboard'>
}
const updateStep = () => {
const updatedStep = step + 1;
setStep(updatedStep);
};
const onboardingScreenToRender = () => {
switch (step) {
case 0:
return (
<Crew />
);
case 1:
return (
<Service />
);
case 2:
return <Address />;
}
};
return (
<div className={classes.container}>
<Header isDesktop={isDesktop} />
<div className={classes.contentOfContainer}>
<div className={classes.titleHeader}>
Onboarding
</div>
<Stepper stepNumber={step} setStepNumber={setStep} />
{checkIsStoreOnboardedData.isLoading && <Loader />}
</div>
</div>
// <OnboardLoader />
);
};
const mapStateToProps = (state, ownProps) => {
return {
...ownProps,
checkIsStoreOnboardedData: state.checkIsStoreOnboardedForOnboardingReducer
};
};
const mapDispatchToProps = (dispatch) => {
return {
checkIsStoreOnboarded: (payload) => dispatch(checkIsStoreOnboarded(payload)),
clearCheckIsStoreOnboarded: () => dispatch(clearCheckIsStoreOnboarded()),
};
};
export default connect(
mapStateToProps,
mapDispatchToProps,
)(withCookies(OnboardScreen));
You need to dispatch your actions:
export const checkIsStoreOnboarded = (payload) => {
return (dispatch) => {
dispatch(checkIsStoreOnboardedInitiate());
API.getAccountSettings(payload)
.then((response) => {
// here
dispatch(checkIsStoreOnboardedSuccess(response.data));
})
.catch((err) => {
// and here
dispatch(checkIsStoreOnboardedFailure(err)(;
});
};
};
That said: you are writing a very outdated style of Redux here - in modern Redux, all of that would probably be possible with 1/4 of the code. If you are just learning Redux, you are probably following a very outdated tutorial. Modern Redux does not require you to write action type strings or action creators and your reducers can contain mutable logic. Also, it does not use connect unless you are working with legacy class components (which you don't seem to be doing).
I really recommend you to read the official Redux tutorial at https://redux.js.org/tutorials/essentials/part-1-overview-concepts
I'm beginner in redux & hooks. I am working on form handling and trying to call an action through useDispatch hooks but it is calling my action twice.
I'm referring this article.
Here is the example:
useProfileForm.js
import { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchProfile } from '../../../redux/profile/profile.actions';
const useProfileForm = (callback) => {
const profileData = useSelector(state =>
state.profile.items
);
let data;
if (profileData.profile) {
data = profileData.profile;
}
const [values, setValues] = useState(data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProfile());
}, [dispatch]);
const handleSubmit = (event) => {
if (event) {
event.preventDefault();
}
callback();
};
const handleChange = (event) => {
event.persist();
setValues(values => ({ ...values, [event.target.name]: event.target.value }));
};
return {
handleChange,
handleSubmit,
values,
}
};
export default useProfileForm;
Action
export const FETCH_PROFILE_BEGIN = "FETCH_PROFILE_BEGIN";
export const FETCH_PROFILE_SUCCESS = "FETCH_PROFILE_SUCCESS";
export const FETCH_PROFILE_FAILURE = "FETCH_PROFILE_FAILURE";
export const ADD_PROFILE_DETAILS = "ADD_PROFILE_DETAILS";
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
function getProfile() {
return fetch("url")
.then(handleErrors)
.then(res => res.json());
}
export function fetchProfile() {
return dispatch => {
dispatch(fetchProfileBegin());
return getProfile().then(json => {
dispatch(fetchProfileSuccess(json));
return json;
}).catch(error =>
dispatch(fetchProfileFailure(error))
);
};
}
export const fetchProfileBegin = () => ({
type: FETCH_PROFILE_BEGIN
});
export const fetchProfileSuccess = profile => {
return {
type: FETCH_PROFILE_SUCCESS,
payload: { profile }
}
};
export const fetchProfileFailure = error => ({
type: FETCH_PROFILE_FAILURE,
payload: { error }
});
export const addProfileDetails = details => {
return {
type: ADD_PROFILE_DETAILS,
payload: details
}
};
Reducer:
import { ADD_PROFILE_DETAILS, FETCH_PROFILE_BEGIN, FETCH_PROFILE_FAILURE, FETCH_PROFILE_SUCCESS } from './profile.actions';
const INITIAL_STATE = {
items: [],
loading: false,
error: null
};
const profileReducer = (state = INITIAL_STATE, action) => {
switch (action.type) {
case ADD_PROFILE_DETAILS:
return {
...state,
addProfileDetails: action.payload
}
case FETCH_PROFILE_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_PROFILE_SUCCESS:
return {
...state,
loading: false,
items: action.payload.profile
};
case FETCH_PROFILE_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
items: []
};
default:
return state;
}
}
export default profileReducer;
**Component:**
import React from 'react';
import { connect } from 'react-redux';
import useProfileForm from './useProfileForm';
import { addProfileDetails } from '../../../redux/profile/profile.actions';
const EducationalDetails = () => {
const { values, handleChange, handleSubmit } = useProfileForm(submitForm);
console.log("values", values);
function submitForm() {
addProfileDetails(values);
}
if (values) {
if (values.error) {
return <div>Error! {values.error.message}</div>;
}
if (values.loading) {
return <div>Loading...</div>;
}
}
return (
<Card>
...some big html
</Card>
)
}
const mapDispatchToProps = dispatch => ({
addProfileDetails: details => dispatch(details)
});
export default connect(null, mapDispatchToProps)(EducationalDetails);
Also when I'm passing data from const [values, setValues] = useState(data); useState to values then ideally I should receive that in component but I'm not getting as it is showing undefined.
const { values, handleChange, handleSubmit } = useProfileForm(submitForm);
values is undefined
The twice dispatch of action is probably because you have used React.StrictMode in your react hierarchy.
According to the react docs, in order to detect unexpected sideEffects, react invokes a certain functions twice such as
Functions passed to useState, useMemo, or useReducer
Now since react-redux is implemented on top of react APIs, actions are infact invoked twice
Also when I'm passing data from const [values, setValues] = useState(data); useState to values then ideally I should receive that in component but I'm not getting as it is showing undefined.
To answer this question, you must know that values is not the result coming from the response of dispatch action from reducer but a state that is updated when handleChange is called so that is supposed to remain unaffected by the action
I think you mean to expose the redux data from useProfileForm which forgot to do
const useProfileForm = (callback) => {
const profileData = useSelector(state =>
state.profile.items
);
let data;
if (profileData.profile) {
data = profileData.profile;
}
const [values, setValues] = useState(data);
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchProfile());
}, [dispatch]);
const handleSubmit = (event) => {
if (event) {
event.preventDefault();
}
callback();
};
const handleChange = (event) => {
event.persist();
setValues(values => ({ ...values, [event.target.name]: event.target.value }));
};
return {
handleChange,
handleSubmit,
values,
data // This is the data coming from redux store on FetchProfile and needs to logged
}
};
export default useProfileForm;
You can use the data in your component like
const { values, handleChange, handleSubmit, data } = useProfileForm(submitForm);
My current React Native Expo app has a ScrollView that implements RefreshControl. A user pulling down the ScrollView will cause the onRefresh function to be executed, which in turns call an action creator getSpotPrices that queries an API using axios.
Problem: If there is a network problem, the axios.get() function will take very long to time out. Thus, there is a need to implement the timing out of either axios.get() or onRefresh.
How can we implement a timeout function into RefreshControl?
/src/containers/main.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ScrollView, RefreshControl } from 'react-native';
import MyList from '../components/MyList';
import { getSpotPrices } from '../actions';
class RefreshableList extends Component {
onRefresh = () => {
this.props.getSpotPrices();
}
render() {
return (
<ScrollView
refreshControl={
<RefreshControl
refreshing={this.props.isLoading}
onRefresh={this._onRefresh}
/>
}>
<MyList />
</ScrollView>
)
}
}
const mapStateToProps = (state) => {
return {
isLoading: state.currencies.isLoading,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getSpotPrices: () => dispatch(getSpotPrices()),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(RefreshableList);
/src/actions/index.js
import api from "../utils/api";
import * as types from "../types";
import Axios from "axios";
const getSpotPrice = async () => {
try {
const res = await Axios.get(`https://api.coinbase.com/v2/prices/spot`);
return parseFloat(res.data.data.amount);
} catch (err) {
throw new Error(err);
}
};
export const getSpotPrices = () => async dispatch => {
try {
const price = await getSpotPrice();
dispatch({
type: types.CURRENCIES_SET,
payload: price
});
} catch (err) {
dispatch({
type: types.CURRENCIES_FAILED_FETCH,
payload: err.toString()
});
} finally {
dispatch({
type: types.CURRENCIES_IS_LOADING,
payload: false
})
}
};
/src/reducers/currencies.js
import * as types from "../types";
const initialState = {
data: {},
isLoading: false,
};
export default (state = initialState, { type, payload }) => {
switch (type) {
case types.CURRENCIES_SET:
return {
...state,
data: payload,
error: "",
isLoading: false
};
case types.CURRENCIES_FAILED_FETCH:
return {
...state,
error: payload,
isLoading: false
};
case types.CURRENCIES_IS_LOADING:
return {
isLoading: payload
}
default:
return state;
}
};
Check if user is connected internet or not using the react-native-netinfo library
NetInfo.fetch().then(state => {
console.log("Connection type", state.type);
console.log("Is connected?", state.isConnected);
this.setState({ connected: state.isConnected });
});
// Subscribe
const unsubscribe = NetInfo.addEventListener(state => {
console.log("Connection type", state.type);
this.setState({ connected: state.isConnected });
});
// Unsubscribe
unsubscribe(); <- do this in componentwillunmount
Its generally a good practice to add a timeout, in all your api calls, in axios you can easily add a timeout option like:
await axios.get(url, { headers, timeout: 5000 })
so in your case modify the axios call as
await Axios.get(https://api.coinbase.com/v2/prices/spot, { timeout: 5000 } );
I have put timeout of 5 seconds you can modify the parameter according to your need.