context is giving me undefined - reactjs

I am uncertain why after the initial load the values in context becomes undefined.
The way I have my context written up is:
export const ProductListContext = createContext({});
export const useListProductContext = () => useContext(ProductListContext);
export const ListProductContextProvider = ({ children }) => {
const [listProduct, setListProduct] = useState({
images: [],
title: "Hello",
});
return (
<ProductListContext.Provider value={{ listProduct, setListProduct }}>
{children}
</ProductListContext.Provider>
);
};
On the initial load of my component. I so get the listProduct to be correct as a console.log will produce
the list is Object {
"images": Array [],
"title": "Hello",
}
The problem is when I try to read listProduct again after it says it is undefined unless I save it to a useState. Any help on this is appreciated. The problem is within the pickImage function
// Initial has all properties correctly
const { listProduct, setListProduct } = useListProductContext();
// Seems to work at all times when I save it here
const [product] = useState(listProduct);
console.log('the list product listed is ', listProduct);
useEffect(() => {
(async () => {
if (Platform.OS !== 'web') {
const {
status,
} = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('Sorry, we need camera roll permissions to make this work!');
}
}
})();
}, []);
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
exif: true,
});
// PROBLEM - listProduct is undefined
console.log('before copy it is ', listProduct);
const listProduct = { ...product };
console.log('the list is', listProduct);
listProduct.images.push(result.uri);
// listProduct.images.push(result.uri);
// const images = listProduct.images;
// images.push(result.uri);
setListProduct({ ...listProduct });
return;
};

Your useListProductContext is violating the rules of hooks, as React sees the use qualifier to validate the rules of hooks.
Rules of Hooks
Using a Custom Hook
"Do I have to name my custom Hooks starting with “use”? Please do. This convention is very important. Without it, we wouldn’t be able to automatically check for violations of rules of Hooks because we couldn’t tell if a certain function contains calls to Hooks inside of it."

Related

Why is React Native AsyncStorage not updating state on mount?

When I try to load the state from AsyncStorage for the screen I just navigated to, I am getting this error:
TypeError: undefined is not an object (evaluating 'weights[numExercise].map') It is trying to use the initial state that the screen initializes the state with, but I want the state to be loaded with the data that I specifically try to load it with on mount, within my useEffect hook.
const WorkoutScreen = ({ navigation, route }) => {
const [workoutName, setWorkoutName] = useState("");
const [exercisesArr, setExercisesArr] = useState([""]);
// Each array inside the arrays (weights & reps), represents an exercise's sets.
const [weights, setWeights] = useState([[""]]);
const [reps, setReps] = useState([[""]]);
const [restTimers, setRestTimers] = useState([""]);
useEffect(() => {
try {
console.log("loading workoutscreen data for:", route.params.name);
const unparsedWorkoutData = await AsyncStorage.getItem(route.params.name);
if (unparsedWorkoutData !== null) {
// We have data!
const workoutData = JSON.parse(unparsedWorkoutData);
setWorkoutName(route.params.name.toString());
setExercisesArr(workoutData[0]);
setWeights(workoutData[1]);
setReps(workoutData[2]);
setRestTimers(workoutData[3]);
}
} catch (error) {
// Error retrieving data
console.log("ERROR LOADING DATA:", error);
}
}, []);
Then further down the line in a component it realizes the error because, again, it's using the initialized state for the weights state.
Return (
{weights[numExercise].map((weight, i) => {
return (
<SetComponent
key={i}
numSet={i}
numExercise={numExercise}
prevWeights={prevWeights}
weights={weights}
setWeights={setWeights}
prevReps={prevReps}
reps={reps}
setReps={setReps}
isDoneArr={isDoneArr}
setIsDoneArr={setIsDoneArr}
/>
);
})}
);
I've made sure that the data is being stored, loaded, and used correctly, so (I think) I've narrowed it down to be something asynchronous; whether it's the setting of the state or loading from storage, I don't know and I can't find a solution. I am new to React Native and would love some suggestions, thank you!
It turns out that using multiple states was causing an issue, I'm assuming because it's asynchronous. So instead I used one state that held an object of states, like so:
const [states, setStates] = useState({
workoutName: "",
exercisesArr: [""],
weights: [[""]],
reps: [[""]],
restTimers: [""],
isDoneArr: [[false]],
originalWorkoutName: "",
});
The data was loaded as such:
const loadWorkoutData = async () => {
try {
console.log("loading workoutscreen data for:", route.params.name);
const unparsedWorkoutData = await AsyncStorage.getItem(route.params.name);
if (unparsedWorkoutData !== null) {
// We have data!
const workoutData = JSON.parse(unparsedWorkoutData);
setStates({
workoutName: route.params.name,
exercisesArr: workoutData[0],
weights: workoutData[1],
reps: workoutData[2],
restTimers: workoutData[3],
isDoneArr: workoutData[4],
originalWorkoutName: route.params.name,
});
}
} catch (error) {
// Error retrieving data
console.log("ERROR LOADING DATA:", error);
}
};

Jest - assert after context update

I have a state that looks like this:
const state = {
departmentIds: [],
employeeIds: [],
};
This state is fed into a context with a function that updates it. So my consumer ends up looking like this:
const {
state: {departmentIds, employeesIds},
updateReport,
} = useReportContext();
The update function is called inside a onChange handler:
const onChangeHandler = (departmentIds: number) => {
updateReport({departmentIds});
};
This is the most important part: Inside my component I have a boolean that heavily depends on these context values. It looks like this:
const shouldClearSelection = departmentIds.length > 0;
This boolean is used in a useEffect for the following:
useEffect(() => {
if (shouldClearSelection) {
updateReport({employeesIds: []});
}
}, [shouldClearSelection, updateReport]);
What I need is to test the state so that if departmentIds are added, the updateReport is called with {employeesIds: []}.
I tried everything honestly, however I still don't manage to fully understand jest methods and how to properly mock. This is the solution I tried the most expecting it'd work:
describe('Departments', () => {
test('should clear employee selection', async () => {
const updateReport = jest.fn();
// #ts-ignore
jest.spyOn(context, 'useReportContext').mockImplementation(() => ({
state: {
departmentIds: null,
employeesIds: [1, 2, 3, 4],
},
updateReport,
}));
render(<Departments />, {
wrapper: AppProvider,
});
updateReport({departmentIds: [1]});
// departmendIds.length is 1 now, thus making the shouldClearSelection true and executing the useEffect.
await waitFor(() => {
expect(updateReport).toHaveBeenCalledWith({employeesIds: []});
});
});
});
However when I log values from the context I never see it update, and the test of course always fails.

Using useState of complex object not working as expected in ReactJS

I have a function component and I am declaring a useState for a complex object like this:
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
I now try to set the state of the order by calling setOrder in a useEffect like this:
useEffect(() => {
(async function() {
let dh = new DataInterface("some string");
let errMsg = "";
// Get the sales order.
try
{
await dh.FetchOrder();
}
catch(error: any)
{
errMsg = error;
};
setOrder(salesOrder => ({...salesOrder, IsRetrieving: false, ErrorMsg: errMsg, DataInterface: dh}));
})();
}, []);
As is, this seems to work fine. However, I have a setInterval object that changes the screen message while order.IsRetrieving is true:
const [fetchCheckerCounter, setFetchCheckerCount] = useState<number>(0);
const statusFetcherWatcher = setInterval(() => {
if (order.IsRetrieving)
{
if (fetchCheckerCounter === 1)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "This can take a few seconds..."}));
}
else if (fetchCheckerCounter === 2)
{
setOrder(salesOrder => ({...salesOrder, RetrievingMsg: "Almost there!.."}));
}
setFetchCheckerCount(fetchCheckerCounter + 1);
}
else
{
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
The issue is that order.IsRetrieving is always true for that code block, even though it does change to false, and my website changes to reflect that, even showing the data from dh.FetchOrder(). That means my timer goes on an infinite loop in the background.
So am I setting the state of order correctly? It's incredibly difficult to find a definite answer on the net, since all the answers are invariably about adding a new item to an array.
Issues
You are setting the interval as an unintentional side-effect in the function body.
You have closed over the initial order.isRetreiving state value in the interval callback.
Solution
Use a mounting useEffect to start the interval and use a React ref to cache the state value when it updates so the current value can be accessed in asynchronous callbacks.
const [order, setOrder] = useState<IMasterState>({
DataInterface: null,
ErrorMsg: "",
IsRetrieving: true,
RetrievingMsg: "Fetching your order status..."
});
const orderRef = useRef(order);
useEffect(() => {
orderRef.current = order;
}, [order]);
useEffect(() => {
const statusFetcherWatcher = setInterval(() => {
if (orderRef.current.IsRetrieving) {
if (fetchCheckerCounter === 1) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "This can take a few seconds...",
}));
} else if (fetchCheckerCounter === 2) {
setOrder(salesOrder => ({
...salesOrder,
RetrievingMsg: "Almost there!..",
}));
}
setFetchCheckerCount(counter => counter + 1);
} else {
// Remove timer.
clearInterval(statusFetcherWatcher);
}
}, 7000);
return () => clearInterval(statusFetcherWatcher);
}, []);

update store on mobx react hook asynchronously

I am trying to fetch the data asynchronously from firestore to my local state using react hooks and mobx and got no clue how to update the store after getting data from firestore. I have used https://github.com/IjzerenHein/firestorter/blob/master/docs/API.md#Collection+query
My store
const store = useObservable({
forms: [],
async initForms(user_id) {
console.log(user_id);
my_forms.query = ref =>
ref
.where('userId', '==', user_id)
.limit(20);
let obj = {};
const arr = [];
my_forms.docs.forEach((doc, i) => {
obj = {
key: i + 1,
title: doc.data.title,
id: doc.id,
tags: doc.data.tags,
category: doc.data.category,
locked: doc.data.locked
};
arr.push(obj);
});
//i can see records from firestore
console.log('arr',arr);
return arr;
},
})
How i tried to update store
useEffect(() => autorun(() => {
store.forms=store.initForms(user_id);
}),[]);
store.forms && (
store.forms.length > 0 ? (
<FuseAnimateGroup
enter={{
animation: "transition.slideUpBigIn"
}}
className="flex flex-wrap py-24"
>
{store.forms.map((f) => {
Well i fixed it by using classic mobx store and using them as the context
These are the steps i did
Create the classic mobx store like we used to do in normal react project
export class MyFormsStore {
forms: []
async initForms(user_id) {
console.log(user_id);
my_forms.query = ref =>
ref
.where('userId', '==', user_id)
.limit(20);
await my_forms.fetch();
let obj = {};
const arr = [];
my_forms.docs.forEach((doc, i) => {
obj = {
key: i + 1,
title: doc.data.title,
id: doc.id,
tags: doc.data.tags,
category: doc.data.category,
locked: doc.data.locked
};
arr.push(obj);
});
this.forms = arr;
}
}
decorate(MyFormsStore, {
forms: observable
})
Export it as the context
export default createContext(new MyFormsStore());
Play with store as the functional component using useContext hook
const Forms = observer((props) => {
const store = useContext(MyFormsStore)
.
.
.
Fetch the data from firestore using useEffect hook
useEffect(() => {
store.initForms(user_id);
}, []);
Check if the store has been initialised or not , it yes render it
{
store.forms !== undefined && (
store.forms.length > 0 ? (
//render
I am still looking how can I memoize expensive functions so that I can avoid calling them on every render using useMemo . Feedbacks will be appreciated
You don't need the useEffect to update data that is tracked by Mobx.
The simplest way is to use runInAction from MobX.
So when your async function is finally resolved, you then use runInAction to update the store. And that will eventually trigger the rendering of react components that are using that particular data.
something like this:
async fetchProjects() {
this.githubProjects = []
this.state = "pending"
try {
const projects = await fetchGithubProjectsSomehow()
const filteredProjects = somePreprocessing(projects)
// after await, modifying state again, needs an actions:
runInAction(() => {
this.state = "done"
this.githubProjects = filteredProjects
})
} catch (error) {
runInAction(() => {
this.state = "error"
})
}
}
You can read more in official documentation:
Writing asynchronous actions

Redux Saga All effects proper usage

I've started working with Redux-saga and I've been following the advanced section for composing sagas in parallel.
Currently this is the function that I'm using with all method, I need these to filter and transform some pages metadata before assigning to the Nav component.
function* CreateNavFromPages(action) {
const menu = action.payload.menu
const menuPagesData = toList(menu.get('pages')).map(
page => {
const title = page.get('title');
const uri = page.get('uri');
return toPageData(title, uri);
}
);
const staticPagesData = menuPagesData.filter(p => p.get('kind') === 'static');
const pages = yield all(
staticPagesData.map(page => backend(PagesApi, page.get('id')))
);
console.log(pages);
const staticPagesMetadata = pages
.filter(({ result, err }) => !err && result.items.length > 0)
.map(result => fromJS(buildMetadata(fromJs(result.items[0]))));
const pagesMetadata = menuPagesData.map(page => {
const key = staticPagesMetadata.findIndex(el => findPage(el, id));
if (key) {
return staticPagesMetadata[key].copy({data: page.get('data')});
}
return page;
});
const nav = Immutable.Map({
position: 'top',
target: 'home',
pagesMetadata
});
const {result, err} = yield backend(Navs.create, nav);
if (err) { ...}
else {
yield put(NavActions.createNavSuccessful(result.nav));
return true;
}
}
function findPage(el, id) {
return el.get('id') === id;
}
backend.js
export default function* backend(...args) {
try {
return { result: yield call(...args)};
}
catch (e) {...}
}
pages_api.js
.....
function get(id) {
return fetch('/api/pages/${id}');
}
export default {get}
When I run this on my App, I get the following error:
uncaught at watchMany
at takeLatest(CREATE_NAV, createNavFromPages)
at createNavFromPages
TypeError: pages.filter is not a function
When checking console.log, I see the following:
{size: 2, _origin: 0, _capacity: 2, _level: 5, _root: null, _tail: {Array(2){Generator, Generator}}}
Is there something I'm missing here?
UPDATED: Added PagesApi definition
As the error suggests the problem is here:
const pages = yield all(
staticPagesData.map(page => backend(PagesApi, page.get('id')))
);
The all effect expects a native javascript array as input parameter, however you are passing down Immutable list (which is the return type of staticPagesData.map).
Since the all effect doesn't understand Immutable.js lists it treats it as any other object - that is it resolves to the object itself, which is what you see in your console.log message.
To fix it you can e.g. convert the immutable list to javascript array like so:
const pages = yield all(
staticPagesData.map(page => backend(PagesApi, page.get('id'))).toJS()
);

Resources