ReactJS How to fetch only if a condition is true? - reactjs

I'm trying to find a proper way to use useFecthApi conditionnally.
I have the following component :
export const DetailedUser: FC = () => {
const userState = useFetchApi(() => findUser(userId))
const fetchContacts = mustFecthContacts() // implemenattion does not matter
return (
<>
<UserInformation userState={userState } />
</>
)
}
type Props = Readonly<{
userState: State<UserDetails>
}>
export const UserInformation : FC<Props> = ({ userState }) => {
}
What I would like to do, is to create a contactState defined by const contactState= useFetchApi(() => findContacts(userId)) only if fetchContacts equals true, and then pass this contactState to UserInformation.
So basically, something like :
export const DetailedUser: FC = () => {
const userState = useFetchApi(() => findUser(userId))
// fetchContacts is a boolean (implementation is not important)
const fetchContacts = mustFecthContacts()
const contactStates = fetchContacts ? useFetchApi(() => findContacts(userId)) : undefined
return (
<>
<UserInformation userState={userState} contactsState = {contactStates } />
</>
)
}
type Props = Readonly<{
userState: State<UserDetails>,
contactStates? : State<ContactDetails>
}>
export const UserInformation : FC<Props> = ({ userState, contactStates }) => {
}
I'm pretty new to react (and to frond en development) so I don't know how to achieve that properly.
Any advice ?
Thanks.

You should use useEffect hook to fetch data. And also useState to store the data locally in a component.
Something like:
// this stores the userState properly
const [userState, setUserState] = useState(() => {
return useFetchApi(() => findContacts(userId))
});
// this triggers everytime fetchContacts changes and will
// fetch new data if fetchContacts is "truthy"
useEffect(() => {
if(!!fetchContacts) {
const userData = useFetchApi(() => findContacts(userId))
setUserState(userData)
}
}, [fetchContacts])

Related

React Component is rerendering, even though it does not depend on the state

In my React code I have to use a legacy component, which makes a setup api call when it is first rendered. The component has a custom completion/cancelation event which I use to trigger a State update. The current Code looks like this:
export const useOneTimePassword = (
headline = "OTP anfordern",
id = "opt",
type = "sms",
businessProcess = "otp-process"
): UseOneTimePasswordReturn => {
const [otpCode, setOtpCode] = useState<undefined | string>();
const [isOtpCancelled, setIsOtpCancelled] = useState<boolean>(false);
const openOtp = () => {
const otp = document.querySelector(`otp-component#${id}`) as OtpElement;
otp.open();
};
const OtpComponent: FC = () => (
<Otp
headline={headline}
id={id}
type={type}
businessProcess={businessProcess}
setIsOtpCancelled={setIsOtpCancelled}
setOtpCode={setOtpCode}
/>
);
return {
otpCode,
isOtpCancelled,
openOtp,
OtpComponent,
removeOtp: () => {
setOtpCode(undefined);
},
};
};
and for the Component it looks like this:
const Otp: React.FC<OtpProps> = ({
headline,
businessProcess,
type,
id,
setOtpCode,
setIsOtpCancelled,
}) => {
function onOtpResponse(e: CompletedEvent) {
if (e.detail.otpCode) {
setOtpCode(e.detail.otpCode);
setIsOtpCancelled(false);
} else {
setIsOtpCancelled(true);
}
}
const ref = useRef();
useEffect(() => {
//#ts-ignore
if (ref.current) ref.current.addEventListener("completed", onOtpResponse);
}, []);
return (
<otp-component
ref={ref}
headline={headline}
id={id}
type={type}
business-process={businessProcess}
/>
);
};
export default Otp;
What I do not understand is that state changes in otpCode aswell as isOtpCancelled cause a rerender of the OtpComponent

What is this react functional component which returns functions instead of template?

Please can someone tell me what is such as react component that returns a bunch of functions and which uses react hooks inside of it ?
Even a react functional component is supposed to return jsx or any other template , no ?
Is such as component stated in react documentation ? how is it called ?!
export default function useViewFilter() {
const dispatch = useDispatch();
const { boardId, viewId } = useParams<RouteParamsTypes>();
const [selectedFacets, setSelectedFacets] = useState<SelectedFacets>({});
const [propertiesData, setPropertiesData] = useState<PropertyData[]>([]);
const [propertiesFacetingTypes, setPropertiesFacetingTypes] = useState<PropertiesFacetingTypes>({});
const neededData = useSelector((state: IFrontAppState) => selectFilterHookNeededData(state, { boardId, viewId }));
const isReady = useSelector((state: IFrontAppState) => isViewModelLoaded(state, boardId, viewId));
const storeFacetsFilters = useSelector((state: IFrontAppState) => getViewFacetsFilters(state, boardId, viewId));
const storeFacetingTypes = useSelector((state: IFrontAppState) =>
getViewPropertiesFacetingTypes(state, boardId, viewId)
);
const dataRef = useRef({
initialized: false
});
useEffect(() => {
handleMakeNewSearch(selectedFacets, false);
}, [neededData]);
// last state before refresh
useEffect(() => {
if (isReady && !dataRef.current.initialized) {
dataRef.current.initialized = true;
setSelectedFacets(storeFacetsFilters ? (storeFacetsFilters as SelectedFacets) : {});
setPropertiesFacetingTypes(storeFacetingTypes ? storeFacetingTypes : {});
handleMakeNewSearch(storeFacetsFilters as SelectedFacets, false);
}
}, [isReady, storeFacetsFilters, storeFacetingTypes, dataRef]);
const handleClear = () => {
const selectedFacets = {};
setSelectedFacets(selectedFacets);
handleMakeNewSearch(selectedFacets, true);
dispatch(
actions.ClearViewFilters({
BoardId: boardId,
ViewId: viewId
})
);
};
const handleMakeNewSearch = (selectedFacets: SelectedFacets, doUpdateInBackend: boolean) => {
const { propertiesIds, properties, orderedEntities, boardMembersObj } = neededData;
if (properties && propertiesIds?.length) {
const orderedProperties = propertiesIds?.map(pId => ({
Id: pId,
...properties[pId]
}));
const itemsJsData = makeItemsJsDataFromAllEntities(orderedEntities, orderedProperties, boardMembersObj);
const newSearchResult = makeSearchAggregations(itemsJsData, orderedProperties, selectedFacets);
// set filtred items
if (doUpdateInBackend) {
const filtredEntitiesIds = newSearchResult.items.map(x => x.id) as string[];
dispatch(
actions.UpdateViewFilters({
BoardId: boardId,
ViewId: viewId,
FacetsFilters: selectedFacets,
FiltredEntitiesIds: doesThereIsFeltering(selectedFacets) ? filtredEntitiesIds : null,
PropertiesFacetingTypes: propertiesFacetingTypes
})
);
}
// update filter panel
const newPropertiesData = makeNewPropertiesData(
propertiesIds,
properties,
newSearchResult.aggregations,
propertiesFacetingTypes
);
setPropertiesData(newPropertiesData);
}
};
return {
propertiesData,
propertiesFacetingTypes,
handleClear,
setPropertiesFacetingTypes
};
}
It's a custom hook.
it's one of the ways (nowadays maybe the standard way) of extracting logic in React function components.

Component rerenders with wrong redux state?

I have a very weird bug that I'm trying to understand for 1.5 days now. The problem with this bug is, that it is very hard to show it without showing around 2000 lines of code - I tried rebuilding a simple example in a codesandbox but couldn't reproduce the bug.
The bug can be easily described, though:
I have a parent component A, and a child component B. Both are connected to the same redux store and subscribed to a reducer called active. Both components print the exact same activeQuestion state property. Both components are connected to the redux store individually via connect()
I dispatch an action SET_ACTIVE_QUESTION and the components rerender (I'm not sure why each re-render happens) and component B now has the updated state from the store and component A doesn't ... and I can't seem to figure out why that is.
The real application is fairly big but there are a couple of weird things that I observed:
The bug disappears when I subscribe the parent component of A to the active state (Component A is subscribed itself).
The action to change the active question is qued before it is fired with setTimeout(() => doAction(), 0). If I remove the setTimeout the bug disappears.
Here is why I think this question is relevant even without code: How is it even possible that an action is dispatched in the redux store (the first console log is directly from the reducer) and the wrong state is displayed on a subsequent render? I'm not sure how this could even be possible unless its a closure or something.
Update (mapStateToProps) functions:
Component A (wrong state):
const mapStateToProps = (state: AppState) => ({
active: state.active,
answerList: state.answerList,
surveyNotifications: state.surveyNotifications,
activeDependencies: state.activeDependencies,
});
Component B (right state):
const mapStateToProps = (state: AppState) => ({
surveyNotifications: state.surveyNotifications,
active: state.active,
answerList: state.answerList,
activeDependencies: state.activeDependencies,
});
Update:
The state transition is triggered by component B (correct state) with this function:
const goToNextQuestionWithTransition = (
where: string,
shouldPerformValidation?: boolean
) => {
setInState(false);
setTimeout(() => {
props.goToQuestion(where, shouldPerformValidation);
}, 200);
};
Removing the setTimeout removes the bug (but I don't know why)
Update (show reducer):
export const INITIAL_SATE = {
activeQuestionUUID: '',
...
};
export default function (state = INITIAL_SATE, action) {
switch (action.type) {
case actionTypes.SET_ACTIVE_QUESTION:
console.log('Action from reducer', action)
return { ...state, activeQuestionUUID: action.payload };
...
default:
return {...state};
}
}
Update
Component A - correct state
const Survey: React.FC<IProps> = (props) => {
const {
survey,
survey: { tenantModuleSet },
} = props;
const [isComplete, setIsComplete] = React.useState(false);
const classes = useStyles();
const surveyUtils = useSurveyUtils();
console.log('Log from component A', props.active.activeQuestionUUID)
React.useEffect(() => {
const firstModule = tenantModuleSet[0];
if (firstModule) {
props.setActiveModule(firstModule.uuid);
} else {
setIsComplete(true);
}
}, []);
const orderedLists: IOrderedLists = useMemo(() => {
let orderedQuestionList: Array<string> = [];
let orderedModuleList: Array<string> = [];
tenantModuleSet.forEach((module) => {
orderedModuleList.push(module.uuid);
module.tenantQuestionSet.forEach((question) => {
orderedQuestionList.push(question.uuid);
});
});
return {
questions: orderedQuestionList,
modules: orderedModuleList,
};
}, [survey]);
const validateQuestion = (question: IQuestion) => {
...
};
const findModuleForQuestion = (questionUUID: string) => {
...
};
const { setActiveQuestion, setActiveModule, active } = props;
const { activeQuestionUUID, activeModuleUUID } = props.active;
const currentQuestionIndex = orderedLists.questions.indexOf(
activeQuestionUUID
);
const currentModuleIndex = orderedLists.modules.indexOf(activeModuleUUID);
const currentModule = props.survey.tenantModuleSet.filter(
(module) => module.uuid === active.activeModuleUUID
)[0];
if (!currentModule) return null;
const currentQuestion = currentModule.tenantQuestionSet.filter(
(question) => question.uuid === activeQuestionUUID
)[0];
const handleActiveSurveyScrollDirection = (destination: string) => {
...
};
const isQuestionLastInModule = ...
const moveToNextQuestion = (modules: string[], questions: string[]) => {
if (isQuestionLastInModule) {
if (currentModule.uuid === modules[modules.length - 1]) {
props.setActiveSurveyView("form");
} else {
setActiveQuestion("");
setActiveModule(modules[currentModuleIndex + 1]);
}
} else {
console.log('this is the move function')
setActiveQuestion(questions[currentQuestionIndex + 1]);
}
};
const goToQuestiton = (destination: string, useValidation = true) => {
....
moveToNextQuestion(modules, questions);
};
return (
<section className={classes.view}>
{isComplete ? (
<SurveyComplete />
) : (
<div className={classes.bodySection}>
<Module
// adding a key here is nessesary
// or the Module will not unmount when the module changes
key={currentModule.uuid}
module={currentModule}
survey={props.survey}
goToQuestion={goToQuestiton}
/>
</div>
)}
{!isComplete && (
<div className={classes.footerSection}>
<SurveyFooter
tenantModuleSet={props.survey.tenantModuleSet}
goToQuestion={goToQuestiton}
orderedLists={orderedLists}
/>
</div>
)}
</section>
);
};
const mapStateToProps = (state: AppState) => ({
active: state.active,
answerList: state.answerList,
surveyNotifications: state.surveyNotifications,
activeDependencies: state.activeDependencies,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
removeQuestionNotification,
setActiveQuestion,
setActiveModule,
setActiveSurveyScrollDirection,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(Survey);
Component B (wrong state)
const Question: React.FC<IProps> = (props: IProps) => {
const [showSubmitButton, setShowSubmitButton] = React.useState(false);
const [inState, setInState] = React.useState(true);
const classes = useStyles();
const { question, module, goToQuestion, active } = props;
const notifications: Array<IQuestionNotification> =
props.surveyNotifications[question.uuid] || [];
const answerArr = props.answerList[question.uuid];
const dependency = props.activeDependencies.questions[question.uuid];
useEffect(() => {
/**
* Function that moves to next or previous question based on the activeSurveyScrollDirection
*/
const move =
active.activeSurveyScrollDirection === "forwards"
? () => goToQuestion("next", false)
: () => goToQuestion("prev", false); // backwards
if (!dependency) {
if (!question.isVisible) move();
} else {
const { type } = dependency;
if (type === DependencyTypeEnum.SUBTRACT) {
console.log('DEPENDENCY MOVE')
move();
}
}
}, [dependency, question, active.activeQuestionUUID]);
console.log('Log from component B', active.activeQuestionUUID)
const goToNextQuestionWithTransition = (
where: string,
shouldPerformValidation?: boolean
) => {
// props.goToQuestion(where, shouldPerformValidation);
setInState(false);
setTimeout(() => {
props.goToQuestion(where, shouldPerformValidation);
}, 200);
};
/**
* Questions that only accept one answer will auto submit
* Questions that have more than one answer will display
* complete button after one answer is passed.
*/
const doAutoComplete = () => {
if (answerArr?.length) {
if (question.maxSelect === 1) {
goToNextQuestionWithTransition("next");
}
if (question.maxSelect > 1) {
setShowSubmitButton(true);
}
}
};
useDidUpdateEffect(() => {
doAutoComplete();
}, [answerArr]);
return (
<Grid container justify="center">
<Grid item xs={11} md={8} lg={5}>
<div className={clsx(classes.question, !inState && classes.questionOut)}>
<QuestionBody
question={question}
notifications={notifications}
module={module}
answerArr={answerArr}
/>
</div>
{showSubmitButton &&
active.activeQuestionUUID === question.uuid ? (
<Button
variant="contained"
color="secondary"
onClick={() => goToNextQuestionWithTransition("next")}
>
Ok!
</Button>
) : null}
</Grid>
</Grid>
);
};
const mapStateToProps = (state: AppState) => ({
surveyNotifications: state.surveyNotifications,
active: state.active,
answerList: state.answerList,
activeDependencies: state.activeDependencies,
});
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
setActiveQuestion,
},
dispatch
);
export default connect(mapStateToProps, mapDispatchToProps)(Question);
Can you post a copy of the mapStateToProps of both component B and component A? If you are using reselect (or similar libraries), can you also post the selectors definitions?
Where are you putting the setTimeout() call?
If you are sure that there are no side effects within the mapStateToProps then it seems that you are mutating the activeQuestion property somewhere before or after the component B re-renders, assigning the old value. (Maybe you have to search for some assignement in conditions).
Also note that you can not always trust the console log, as it's value can be evaluated at later time the you call it.

Modifying object inside array with useContext

I've been having trouble using React's useContext hook. I'm trying to update a state I got from my context, but I can't figure out how. I manage to change the object's property value I wanted to but I end up adding another object everytime I run this function. This is some of my code:
A method inside my "CartItem" component.
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
The "Cart Component" which renders the "CartItem"
const { cart, catalogue } = useContext(ShoppingContext);
const [catalogueValue] = catalogue;
const [cartValue, setCartValue] = cart;
const quantiFyCartItems = () => {
let arr = catalogueValue.map((item) => item.name);
let resultArr = [];
arr.forEach((item) => {
resultArr.push(
cartValue.filter((element) => item === element.name).length
);
});
return resultArr;
};
return (
<div>
{cartValue.map((item, idx) => (
<div key={idx}>
<CartItem
name={item.name}
price={item.price}
quantity={item.quantity}
id={item.id}
/>
<button onClick={quantiFyCartItems}>test</button>
</div>
))}
</div>
);
};
So how do I preserve the previous objects from my cartValue array and still modify a single property value inside an object in such an array?
edit: Here's the ShoppingContext component!
import React, { useState, createContext, useEffect } from "react";
import axios from "axios";
export const ShoppingContext = createContext();
const PRODUCTS_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/products.json";
const VOUCHER_ENDPOINT =
"https://shielded-wildwood-82973.herokuapp.com/vouchers.json";
export const ShoppingProvider = (props) => {
const [catalogue, setCatalogue] = useState([]);
const [cart, setCart] = useState([]);
const [vouchers, setVouchers] = useState([]);
useEffect(() => {
getCatalogueFromApi();
getVoucherFromApi();
}, []);
const getCatalogueFromApi = () => {
axios
.get(PRODUCTS_ENDPOINT)
.then((response) => setCatalogue(response.data.products))
.catch((error) => console.log(error));
};
const getVoucherFromApi = () => {
axios
.get(VOUCHER_ENDPOINT)
.then((response) => setVouchers(response.data.vouchers))
.catch((error) => console.log(error));
};
return (
<ShoppingContext.Provider
value={{
catalogue: [catalogue, setCatalogue],
cart: [cart, setCart],
vouchers: [vouchers, setVouchers],
}}
>
{props.children}
</ShoppingContext.Provider>
);
};
edit2: Thanks to Diesel's suggestion on using map, I came up with this code which is doing the trick!
const newCartValue = cartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
if (boolean && item.quantity < item.available) {
item.quantity++;
}
return item;
});
removeFromStock();
setCartValue(() => [...newCartValue]);
};```
I'm assuming that you have access to both the value and the ability to set state here:
const addToQuantity = () => {
cartValue.forEach((item) => {
let boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
setCartValue((currentState) => [...currentState, item.quantity++])
} else {
return null;
}
});
};
Now... if you do [...currentState, item.quantity++] you will always add a new item. You're not changing anything. You're also running setCartValue on each item, which isn't necessary. I'm not sure how many can change, but it looks like you want to change values. This is what map is great for.
const addToQuantity = () => {
setCartValue((previousCartValue) => {
const newCartValue = previousCartValue.map((item) => {
const boolean = Object.values(item).includes(props.name);
console.log(boolean);
if (boolean) {
return item.quantity++;
} else {
return null;
}
});
return newCartValue;
});
};
You take all your values, do the modification you want, then you can set that as the new state. Plus it makes a new array, which is nice, as it doesn't mutate your data.
Also, if you know only one item will ever match your criteria, consider the .findIndex method as it short circuits when it finds something (it will stop there), then modify that index.

Set value to textfield with hooks and redux material ui

I'm building an app using react, redux, and redux-saga.
The situation is that I'm getting information from an API. In this case, I'm getting the information about a movie, and I will update this information using a basic form.
What I would like to have in my text fields is the value from the object of the movie that I'm calling form the DB.
This is a brief part of my code:
Im using 'name' as an example.
Parent component:
const MovieForm = (props) => {
const {
movie,
} = props;
const [name, setName] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({
name,
});
};
const handleSetValues = () => {
console.log('hi');
console.log(movie, name);
setName(movie.name);
setValues(true);
};
useEffect(() => {
if (movie && values === false) {
handleSetValues();
}
});
return (
<Container>
<TextField
required
**defaultValue={() => {
console.log(movie, name);
return movie ? movie.name : name;
}}**
label='Movie Title'
onChange={(e) => setName(e.target.value)}
/>
</Container>
);
};
export default MovieForm;
....
child component
const MovieUpdate = (props) => {
const { history } = props;
const { id } = props.match.params;
const dispatch = useDispatch();
const loading = useSelector((state) => _.get(state, 'MovieUpdate.loading'));
const created = useSelector((state) => _.get(state, 'MovieUpdate.created'));
const loadingFetch = useSelector((state) =>
_.get(state, 'MovieById.loading')
);
const movie = useSelector((state) => _.get(state, 'MovieById.results'));
useEffect(() => {
if (loading === false && created === true) {
dispatch({
type: MOVIE_UPDATE_RESET,
});
}
if (loadingFetch === false && movie === null) {
dispatch({
type: MOVIE_GET_BY_ID_STARTED,
payload: id,
});
}
});
const updateMovie = (_movie) => {
const _id = id;
const obj = {
id: _id,
name: _movie.name,
}
console.log(obj);
dispatch({
type: MOVIE_UPDATE_STARTED,
payload: obj,
});
};
return (
<div>
<MovieForm
title='Update a movie'
buttonTitle='update'
movie={movie}
onCancel={() => history.push('/app/movies/list')}
onSubmit={updateMovie}
/>
</div>
);
};
export default MovieUpdate;
Then, the actual problem is that when I use the default prop on the text field the information appears without any problem, but if i use defaultValue it is empty.
Ok, I kind of got the answer, I read somewhere that the defaultValue can't be used int the rendering.
So I cheat in a way, I set the properties multiline and row={1} (according material-ui documentation) and I was able to edit this field an receive a value to display it in the textfield

Resources