Converting componentDidMount into a react hook - javascript

I have a project of a mixture of componentDidMount and react hooks(terrible i know) however within one of the componentDidMount's I setState to allow for contractData to match the params of an Id as you can see below
componentDidMount = async() => {
let tasks = [];
try {
tasks = await getTasks(this.props.match.params.id);
}
catch (err) {
if (err !== HttpStatus.NOT_FOUND) {
throw err;
}
}
this.setState({ contract: await getContractData(this.props.match.params.id), tasks: tasks });
}
I need to do something similar about the params but for my getSupplierData within my useEffect
const [suppliers, setSupplierList] = useState([]);
useEffect(() => {
getSupplierData().then(response => {
setSupplierList(response);
});
}, []);
How could i do this?

Related

How to do cleanup in useEffect React?

Usually I do useEffect cleanups like this:
useEffect(() => {
if (!openModal) {
let controller = new AbortController();
const getEvents = async () => {
try {
const response = await fetch(`/api/groups/`, {
signal: controller.signal,
});
const jsonData = await response.json();
setGroupEvents(jsonData);
controller = null;
} catch (err) {
console.error(err.message);
}
};
getEvents();
return () => controller?.abort();
}
}, [openModal]);
But I don't know how to do in this situation:
I have useEffect in Events.js file that get events from function and function in helpers.js file that create events on given dates except holidays (holiday dates fetch from database).
Events.js
useEffect(() => {
if (groupEvents.length > 0) {
const getGroupEvents = async () => {
const passed = await passEvents(groupEvents); // function in helpers.js (return array of events)
if (passed) {
setEvents(passed.concat(userEvents));
} else {
setEvents(userEvents);
}
};
getGroupEvents();
}
}, [groupEvents, userEvents]);
helpers.js
const passEvents = async (e) => {
try {
const response = await fetch(`/api/misc/holidays`, {
credentials: 'same-origin',
});
const jsonData = await response.json();
const holidays = jsonData.map((x) => x.date.split('T')[0]); // holiday dates
return getEvents(e, holidays); // create events
} catch (err) {
console.error(err.message);
}
};
You can either not clean up, which really is also fine in many situations, but if you definitely want to be able to abort the in-flight request, you will need to create the signal from the top-level where you want to be able to abort, and pass it down to every function.
This means adding a signal parameter to passEvents.
Remove ?:
return () => controller.abort();
Like to this: https://www.youtube.com/watch?v=aKOQtGLT-Yk&list=PL4cUxeGkcC9gZD-Tvwfod2gaISzfRiP9d&index=25
Is it what you want?

React context returns promise

I have a todo app. Im trying to use context api(first time). I have add, delete and get functions in context. I can use add and delete but cant return the get response to state. It returns promise if i log; context. Im using async await. I tried almost everything i know but cant solve it. Where is my fault ?
Thank you.
task-context.js
import React, { useReducer } from "react";
import TaskContext from "./task-actions";
import { TaskReducer, ADD_TASK, GET_TASKS, REMOVE_TASK } from "./reducers";
const GlobalState = (props) => {
const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });
const addTask = (task) => {
dispatch({ type: ADD_TASK, data: task });
};
const removeTask = (taskId) => {
dispatch({ type: REMOVE_TASK, data: taskId });
};
const getTasks = () => {
dispatch({ type: GET_TASKS });
};
return (
<TaskContext.Provider
value={{
tasks: tasks,
getTasks: getTasks,
addTask: addTask,
removeTask: removeTask,
}}
>
{props.children}
</TaskContext.Provider>
);
};
export default GlobalState;
reducers.js
import taskService from "../Services/tasks-service";
export const ADD_TASK = "ADD_TASK";
export const GET_TASKS = "GET_TASKS";
export const REMOVE_TASK = "REMOVE_TASK";
const addTask = async (data, state) => {
console.log("Adding : " + data.title);
try {
let task = {
title: data.title,
description: data.description,
comment: data.comment,
progress: data.status
};
const res = await taskService.addNewTask(task);
console.log(res);
if (res) {
getTasks();
}
} catch (err) {
console.log(err);
}
return;
};
const getTasks = async () => {
let response = {}
try {
const res = await taskService.loadTasks();
response = res.data
} catch (err) {
console.log(err);
}
return { tasks: response }
};
const removeTask = async (data) => {
try {
await taskService.deleteTask(data.id);
} catch (err) {
console.log(err);
}
};
export const TaskReducer = (state, action) => {
switch (action.type) {
case ADD_TASK:
return addTask(action.data);
case GET_TASKS:
console.log(getTasks());
return getTasks();
case REMOVE_TASK:
return removeTask(action.data);
default:
return state;
}
};
task-actions.js
import React from "react";
export default React.createContext({
addTask: (data) => {},
removeTask: (data) => {},
getTasks: () => {}
});
To start with, you are getting promises returned because you are explicitly returning promises: return addTask(action.data). All your actions are returning promises into the reducer.
A reducer should be a pure function, meaning that it does not have any side effects (call code outside its own scope), or contain any async functionality, and it should return the same data given the same inputs every single time. You've essentially got the workflow back to front.
There's a lot to unpick here so I'm going to provide pseudocode rather than try and refactor the entire service, which you will have a more complete understanding of. Starting with the reducer:
export const TaskReducer = (state, action) => {
switch (action.type) {
case ADD_TASK:
return [...state, action.data];
case GET_TASKS:
return action.data;
case REMOVE_TASK:
return state.filter(task => task.id !== action.data.id);
default:
return state;
}
};
This reducer describes how the state is updated after each action is complete. All it should know how to do is update the state object/array it is in charge of. When it comes to fetching data, calling the reducer should be the very last thing you have to do.
Now on to the actions. The add action is a problem because its not actually returning any data. On top of that, it calls getTasks when really all it ought to do is return one added task (which should be getting returned from await taskService.addNewTask). I would expect that res.data is actually a task object, in which case:
export const addTask = async (data) => {
try {
const task = {
title: data.title,
description: data.description,
comment: data.comment,
progress: data.status
};
const res = await taskService.addNewTask(task);
return res.data;
} catch (err) {
return err;
}
};
Similarly for getTasks, I'm going to assume that await taskService.loadTasks returns an array of task objects. In which case, we can simplify this somewhat:
export const getTasks = async () => {
try {
const res = await taskService.loadTasks();
return res.data;
} catch (err) {
return err;
}
};
Your removeTask action is essentially fine, although you will want to return errors instead of just logging them.
Notice we're now exporting these actions. That is so we can now call them from within GlobalState. We're running into issues with name collision so I've just underscored the imported actions for demo purposes. In reality, it might be better to move all the functionality we did in the last step into your taskService, and import that straight into GlobalState instead. Since that's implementation specific I'll leave it up to you.
import {
TaskReducer,
ADD_TASK,
GET_TASKS,
REMOVE_TASK,
addTask as _addTask,
getTasks as _getTasks,
removeTask as _removeTask,
} from "./reducers";
const GlobalState = (props) => {
const [tasks, dispatch] = useReducer(TaskReducer, { tasks: [] });
const addTask = async (task) => {
const added = await _addTask();
if (added instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: ADD_TASK, data: added });
};
const removeTask = async (taskId) => {
const removed = await _removeTask(taskId);
if (removed instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: REMOVE_TASK, data: taskId });
};
const getTasks = async () => {
const tracks = await _getTracks();
if (tracks instanceof Error) {
// handle error within the application
return;
};
dispatch({ type: GET_TASKS, data: tracks });
};
...
}
Hopefully now you can see how the workflow is supposed to progress. First we call for data from our backend or other API, then we handle the response within the application (for instance, dispatching other actions to notify about errors or side effects of the new data) and then finally dispatch the new data into our state.
As stated at the beginning, what I've provided is essentially pseudocode, so don't expect it to work out of the box.

useState set call not reflecting change immediately prior to first render of app

New to React Hooks and unsure how to solve. I have the following snippet of code within my App.js file below.
What I am basically trying to achieve is to get the user logged in by calling the getUser() function and once I have the user id, then check if they are an authorised user by calling the function checkUserAccess() for user id.
Based on results within the the validIds array, I check to see if it's true or false and set authorised state to true or false via the setAuthorised() call.
My problem is, I need this to process first prior to performing my first render within my App.js file.
At the moment, it's saying that I'm not authroised even though I am.
Can anyone pls assist with what I am doing wrong as I need to ensure that authorised useState is set correctly prior to first component render of application, i.e. path="/"
const [theId, setTheId] = useState('');
const [authorised, setAuthorised] = useState(false);
const checkUserAccess = async (empid) => {
try {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
if (isAuthorised) {
setAuthorised(true)
} else {
setAuthorised(false)
}
} catch (err) {
console.error(err.message);
}
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
setTheId(theId);
checkUserAccess(theId);
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
Unless you are wanting to partially render when you get the user ID, and then get the access level. There is no reason to have multiple useState's / useEffect's.
Just get your user and then get your access level and use that.
Below is an example.
const [user, setUser] = useState(null);
const checkUserAccess = async (empid) => {
const response = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(empid);
return isAuthorised;
}
const getUser = async () => {
try {
const response = await fetch("http://localhost:4200/get-user");
const theId= await response.json();
const access = await checkUserAccess(theId);
setUser({
theId,
access
});
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!user) return <div>Loading</div>;
return <>{user.theId}</>
This way it should work
but keep in mind that you must render your app only if theId in the state is present, which will mean your user is properly fetched.
const [state, setState] = useState({ theId: '', isAutorized: false })
const getUser = async () => {
try {
const idResp = await fetch("http://localhost:4200/get-user");
const theId = await idResp.json();
const authResp = await fetch("http://localhost:4200/get-valid-users");
const allUsers = await authResp.response.json();
const validIds = allUsers.map(({ id }) => id);
const isAuthorised = validIds.includes(theId);
setState({ theId, isAuthorised })
} catch (err) {
console.error(err.message);
}
}
useEffect(() => {
getUser();
}, []);
if (!state.theId) return <div>Loading</div>;
if (state.theId && !isAuthorized) return <AccessNotAllowed />
return <Home />

Test component calling API inside useEffect

I want to test the api call and data returned which should be displayed inside my functional component. I have a component that calls an API when it is first loaded and when certain things change i.e when typing.
I have a useEffect calling the API like so:
useEffect(() => {
const query = queryString;
const apiRequest = async () => {
try {
setIsFetching(true);
const response = await getData(query, page);
setData(response.data);
} catch (error) {
// Do some error
console.log('error');
} finally {
setIsFetching(false);
}
};
apiRequest();
}, [queryString, page]);
getData is an axios function like so:
let getDataRequest;
const getData = (searchQuery = null, page = PAGE, size = PAGE_SIZE) => {
if (getDataRequest) getDataRequest.cancel();
getDataRequest = axios.CancelToken.source();
return axios.get('url_to_api', {
params: {
page,
searchQuery,
size,
},
cancelToken: getDataRequest.token,
});
};
When trying to test this component I am running into the error When testing, code that causes React state updates should be wrapped into act(...):
I have been trying to follow first link also second link third link and many more pages but still no luck.
Here is one version of the test I am still seeing the error:
it('should render data', async () => {
let container;
act(async () => {
await getData.mockResolvedValueOnce({
data: { things: data, meta: { current_page: 1 } },
});
container = render(<Component />);
});
await waitFor(() => container.queryByText('Text I would expect after api call'));
})
I have also tried mocking the function in my test file like so:
import { getData } from './dataAccess';
const mockedData = { data: { things: data } };
jest.mock('./api', () => ({
getData: jest
.fn()
.mockImplementation(() => new Promise(resolve => resolve(mockedData))),
}));
I am using import { act } from 'react-dom/test-utils'; with enzyme and jest. I also am using '#testing-library/react';

ReactJS - push method with useState array

The purpose here is to have an array of channels id where I can populate him with information that is coming from my firebase.
I have my component like this:
export default function Component() {
const [channelsId, setChannelsId] = useState([])
// and I call this function passing my state
useEffect(() => {
getChannelsIds(someId, channelsId, setChannelsId)
}, [])
The function:
export const getChannelsIds = (someId, channelsId, setChannelsId) => {
try {
firestore.collection("channels").where("someId", "==", someId).get().then(querySnapshot => {
querySnapshot.forEach(doc => {
setChannelsId([...channelsId, doc.data().id])
})
})
} catch (err) {
toast.error('Error while trying to get the channel.')
}
}
It's not working, because my channelsId state is being override and I only have the last channelId, console.log screenshot:
You should either use functional updates
export const getChannelsIds = (someId, channelsId, setChannelsId) => {
try {
firestore.collection("channels").where("someId", "==", someId).get().then(querySnapshot => {
querySnapshot.forEach(doc => {
setChannelsId(ids => [...ids, doc.data().id]);
})
})
} catch (err) {
toast.error('Error while trying to get the channel.')
}
}
Or even better you could create first an array with the new data and only update the state once.
export const getChannelsIds = (someId, channelsId, setChannelsId) => {
try {
firestore.collection("channels").where("someId", "==", someId).get().then(querySnapshot => {
const newChannelIds = querySnapshot.map(doc => doc.data().id);
setChannelsId([...channelsId, ...newChannelIds);
});
}
catch (err) {
toast.error('Error while trying to get the channel.')
}
}

Categories

Resources