Code Class is
interface Props {
newEquipmentStats?: BaseEquipmentStats | null,
onEquipmentStatsUpdate: (equipmentStats: BaseEquipmentStats) => BaseEquipmentStats,
}
export default function CreateEquipmentStats(props: Props): JSX.Element {
const { newEquipmentStats, onEquipmentStatsUpdate } = props;
const [equipmentStats, setEquipmentStats] = useState<BaseEquipmentStats>(newEquipmentStats ?? DefaultEquipment.create());
const handleEncumbranceIncrease = () => {
setEquipmentStats((prev_state) => ({
...prev_state,
encumbrance: equipmentStats.encumbrance++,
}));
setEquipmentStats(onEquipmentStatsUpdate(equipmentStats));
}
const handleOnPriceCommit = (value: number) => {
console.log(Number(value))
setEquipmentStats((prev_state) => ({
...prev_state,
price: Number(value)
}));
setEquipmentStats(onEquipmentStatsUpdate(equipmentStats));
}
I am getting methods like handleEncumbranceIncrease to update state correctly, but I am having issues with handleOnPriceCommit I can not seem to get price to ever update without using atomic operators. I am using React 17 if that effects anything.
I have a context that is used to create, update, list and delete a set of data.
It is created as follows with the following types to be returned.
type TDataContext = {
loading: boolean,
getDataList: () => TData[],
addNewData : (data: TData) => Promise<boolean> ,
editData: (dataId: string, data: TData) => Promise<boolean>,
deleteData: (dataId: string) => Promise<boolean>,
}
const ADataContext = createContext<TGymClientDataContext>({
// How to initialize these here ?
})
export const ADataProvider: FC<{}> = ({children}) => {
const [loading, setLoading] = useState(true)
const [snapshot, loading, error] = useCollection<TDataStore>(db.collection(FIRESTORE_COLLECTIONS.DATA_COLLECTION), {});
const getDataList = async (): TData[] => {
if(loading == true) {
return []
}
if(typeof snapshot == "undefined"){
return []
}
return snapshot?.docs.map((item) => {
const fetched = item.data();
return ({
id: item.id,
doj: fetched.doj.toDate(),
firstName: fetched.firstName,
lastName: fetched.lastName,
})
})
}
// ... Function definitions
const values = {
loading,
getDataList,
addNewData,
editData,
deleteData,
}
return <ADataContext.Provider value={values}>
{children}
</ADataContext.Provider>
}
export const useDataHook = () => useContext(ADataContext)
I would like to know what will be the good possible way to pass initial data to createContext ?
I'm creating this with the following usage to be expected
const {loading, getClientList, editClientData, addNewClient, deleteClientData} = useGymClient()
The functions are simply expected to return true or false depending on the operations being successful or not.
Another question is,
The data has to be always caught via getDataList function.
is directly passing the snapshot variable down better approach ? In that case, what will be a better initializer for the create context
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])
I'm working with GoogleBooks API and I can get from the API the object that I need, and from there all the values I want. e.g.
console.log("item", item); // full object
console.log("title", item.volumeInfo.title); // title
but when I want to get nested objects I get an error:
console.log(item.volumeInfo.imageLinks.smallThumbnail);
TypeError: Cannot read property 'smallThumbnail' of undefined
So I created a this:
const BookItem = ({ item }) => {
const dispatch = useDispatch();
...
useEffect(() => {
const objSource = item.volumeInfo.imageLinks;
const getImages = Object.values(objSource);
console.log(getImages);
//eslint-disable-next-line
}, []);
const clickAddToStore = () => {
setAddToStore(true);
};
const clickRemoveFromStore = () => {
setAddToStore(false);
};
const addToDb = () => {
dispatch(
addToWooDb(
item.volumeInfo,
item.searchInfo,
stockQuantity,
bookStatus,
price
)
);
};
return (
<div
className="item3"
onClick={!addToStore ? clickAddToStore : clickRemoveFromStore}
>
{item.volumeInfo.title}
</div>
...
const mapStateToProps = (state) => ({
data: state.items,
loading: state.items.loading,
});
export default connect(mapStateToProps, { addToWooDb })(BookItem);
and works perfectly, hence I can see in the console:
["http://books.google.com/books/content?id=3nMEPQAAC…J&printsec=frontcover&img=1&zoom=5&source=gbs_api", "http://books.google.com/books/content?id=3nMEPQAAC…J&printsec=frontcover&img=1&zoom=1&source=gbs_api"]
0: "http://books.google.com/books/content?id=3nMEPQAACAAJ&printsec=frontcover&img=1&zoom=5&source=gbs_api"
1: "http://books.google.com/books/content?id=3nMEPQAACAAJ&printsec=frontcover&img=1&zoom=1&source=gbs_api"
But strangely I still get an error:
TypeError: Cannot convert undefined or null to object
I think that I can solve it with a promise, but I never understood how they work.
Any idea?
Thanks!
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.