How to make an HTTP hook reusable on the same component - javascript

I have an HTTP hook that can be consumed like this:
const { data, error, isLoading, executeFetch } = useHttp<IArticle[]>('news', []);
In the same component, I want to trigger another API call to POST data and update one of the articles:
const handleChange = (article: IArticle, event: React.ChangeEvent<HTMLInputElement>) => {
executeFetch(`updateNews?id=${article.id}`, { method: 'post', data: { isRead: event.target.checked }});
};
return (
<>
<div className={classes.articleListHeader}>
<h1>Article List</h1>
<small className={classes.headerSubtitle}>{data.length} Articles</small>
</div>
<ul>
{data.map(article => <Article key={article.id} article={article} handleChange={handleChange}/>)}
</ul>
</>
)
My custom hook to fetch data:
export function useHttp<T>(initUrl: string, initData: T): UseHttp<T> {
const initOptions: AxiosRequestConfig = { url: initUrl };
const [options, setOptions] = useState(initOptions);
const useHttpReducer = createHttpReducer<T>();
const [state, dispatch] = useReducer(useHttpReducer, {
isLoading: false,
error: '',
data: initData
});
useEffect(() => {
let cancelRequest = false;
const fetchData = async (cancelRequest: boolean = false) => {
if (!options.url) return;
dispatch({ type: API_REQUEST});
try {
const responsePromise: AxiosPromise<T> = axios(options);
const response = await responsePromise;
if (cancelRequest) return;
dispatch({ type: API_SUCCESS, payload: response.data });
} catch (e) {
console.log("Got error", e);
dispatch({ type: API_ERROR, payload: e.message });
}
};
fetchData(cancelRequest);
return () => {
cancelRequest = true;
}
}, [options]);
const executeFetch = (url: string, options: AxiosRequestConfig = axiosInitialOptions): void => {
options.url = url;
setOptions(options);
};
return { ...state, executeFetch}
The issue is, when I'm doing something like this, the data replaces to the new response (of the POST request), then my UI crashes (no more article list..)
What's the good practice to manage situations like this when I need to call another API in the same component while keeping the reusability of my HTTP hook?
I simply want to execute a POST request somewhere in the component after my GET one - How I can do it in a reusable way and fix my issue?

You can refactoring your custom hook to receive a callback function. I omitted the part of cancelRequest, if you are using axios you can cancel the request via CancelToken:
export function useHttp<T>(initUrl: string): UseHttp<T> {
const initOptions: AxiosRequestConfig = { url: initUrl };
const [options, setOptions] = useState(initOptions);
const useHttpReducer = createHttpReducer<T>();
const [state, dispatch] = useReducer(useHttpReducer, {
isLoading: false,
error: '',
});
const fetchData = async (options, callback) => {
if (!options.url) return;
dispatch({ type: API_REQUEST});
try {
const responsePromise: AxiosPromise<T> = axios(options);
const response = await responsePromise;
dispatch({ type: API_SUCCESS, payload: response.data });
callback(response.data);
} catch (e) {
console.log("Got error", e);
dispatch({ type: API_ERROR, payload: e.message });
}
};
const executeFetch = (url: string, requestOptions: AxiosRequestConfig = axiosInitialOptions, callback): void => {
options.url = url;
fetchData({...options, ...requestOptions}, callback);
};
return { ...state, executeFetch}
};
Usage:
const [articles, setArticles]= useState();
const { error, isLoading, executeFetch } = useHttpRequest();
const handleChange = (article: IArticle, event: React.ChangeEvent<HTMLInputElement>) => {
executeFetch(`updateNews?id=${article.id}`, { method: 'post', data: { isRead: event.target.checked }}, setArticles);
};

Related

Redux actions always in pending state

I am trying to create a scraping application using redux toolkit for learning purposes.Whenever I dispatch the action the data gets scraped and console logged but the action state is never fullfilled and is always pending
MY ASYNC THUNK
export const loadData = createAsyncThunk(
"alldata/getdata",
async ({ pageNo, language }, thunkAPI) => {
const data = await fetch(
`http://localhost:5000/scrape?pageNo=${encodeURIComponent(
pageNo
)}&language=${encodeURIComponent(language)}`
);
const res=await data.json()
return {
payload: res,
};
}
);
MY SLICE
const projectSlice = createSlice({
name: "allprojects",
initialState: {
projectState: [],
workingState: [],
isLoading: false,
hasError: false,
},
reducers: {
addProject: (state, action) => {
return state.workingState.push(action.payload);
},
removeProject: (state, action) => {
return state.workingState.filter(
(project) => project.link !== action.payload.link
);
},
},
extraReducers: {
[loadData.pending]: (state, action) => {
state.isLoading = true;
state.hasError = false;
},
[loadData.fulfilled]: (state, { payload }) => {
state.projectState = payload;
state.isLoading = false;
state.hasError = false;
},
[loadData.rejected]: (state, action) => {
state.isLoading = false;
state.hasError = true;
},
},
});
export const { addProject, removeProject } = projectSlice.actions;
const Projectreducer = projectSlice.reducer;
export default Projectreducer;
export const projectSelector = (state) => state.allprojects;
REACT COMPONENT
const { workingState, projectState, isLoading, hasError } =
useSelector(projectSelector);
const dispatch = useDispatch();
const [selectData, setSelectData] = React.useState({ languages: "" });
const [pageData, setPageData] = React.useState({ pageNo: 1 });
const handleClick = (event) => {
event.preventDefault();
dispatch(
loadData({ pageNo: pageData.pageNo, language: selectData.languages })
);
};
So how do I get the action to be fullfilled and push the data in the ProjectState array after the async request
EDIT:
API
app.get("/scrape", async (req, res) => {
const { pageNo, language } = req.query;
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(
`https://github.com/search?p=${pageNo}&q=language%3A${language}`,
{
waitUntil: "domcontentloaded",
}
); // URL is given by the "user" (your client-side application)
const data = await page.evaluate(() => {
const list = [];
const items = document.querySelectorAll(".repo-list-item");
for (const item of items) {
list.push({
projectName: item.querySelector(".f4 > a").innerText,
about: item.querySelector("p").innerText,
link: item.querySelector("a").getAttribute("href"),
});
}
return list;
});
console.log(data);
await browser.close();
});
Store
import { configureStore } from "#reduxjs/toolkit";
import Projectreducer from "./Slices/slice";
export const store = configureStore({
reducer: {
allprojects: Projectreducer,
},
});
Its possible that the api that you are fetching is throwing an error so in this case it always recommended to have a catch block and throw an error to that its falls into loadData.rejected state.
So, do the check network tab in the dev tools of the browser that you are using, so that you can confirm if the api is responding or not.
Also can you share the projectSelector selector ? could be the something wrong in the selector.
action:
export const loadData = createAsyncThunk(
'alldata/getdata',
async ({ pageNo, language }, { rejectWithValue }) => {
try {
const data = await fetch(
`http://localhost:5000/scrape?pageNo=${encodeURIComponent(
pageNo
)}&language=${encodeURIComponent(language)}`
);
const res = await data.json();
return {
payload: res,
};
} catch (error) {
return rejectWithValue({ payload: error?.message || error });
}
}
);
reducer:
extraReducers: {
...,
[loadData.rejected]: (state, { payload }) => {
state.isLoading = false;
state.hasError = true;
state.message = payload;
},
},
One more thing to mention here is that redux toolkit recommends to use builder callback, for more details check here:
https://redux-toolkit.js.org/api/createslice#extrareducers
in your example it would be like
extraReducers: (builder) => {
builder
.addCase(loadData.pending, (state) => {
...
})
.addCase(loadData.fulfilled, (state, action) => {
...
})
.addCase(loadData.rejected, (state, action) => {
...
});

How can I receive the async data as the resolved value?

I'm trying to read files and convert them to a json, then upload them to a server. But I can't seem to be able to get the data back that isn't a promise. Can you point to what I'm doing wrong?
const onSubmit = async (formData: FormValues) => {
remove();
append(defaultFormValues.documents[0] as object);
setIsLoading(true);
const objToUpload = {
name: formData.documentName,
type: formData.documentType,
contents: [
formData.documents.map(async (document) => {
return {
language: document.language,
data: await readFromFile(document.file[0]),
actions: await readFromFile(document.actions[0]),
};
}),
],
};
console.log(objToUpload);
}
};
const onSubmit = async (formData: FormValues) => {
remove();
append(defaultFormValues.documents[0] as object);
setIsLoading(true);
const data = await Promise.all(formData.documents.map(async (document) => {
return {
language: document.language,
data: await readFromFile(document.file[0]),
actions: await readFromFile(document.actions[0]),
};
}));
const objToUpload = {
name: formData.documentName,
type: formData.documentType,
contents: data,
};
console.log(objToUpload);
};

What is the best way to call a function and render a child component onClick in React?

I have the below code, I want to call a function and render a child component onCLick. What is the best way to achieve this?
import AddOrder from './AddOrder'
return (
<Button onClick={handleCheckout}>Checkout</Button>
)
const handleCheckout = () => {
<AddOrder />
fetch("http://localhost:5000/create-checkout-session", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
items: data?.getUser ? data.getUser.cart : cart,
email: currentUser ? currentUser.email : undefined,
}),
})
.then(async (res) => {
if (res.ok) return res.json();
const json = await res.json();
return await Promise.reject(json);
})
.then(({ url }) => {
window.location = url;
})
.catch((e) => {
console.error(e.error);
});
};
I tried making a new function called handleAll and adding it like this:
function handleAll(){
handleCheckout()
<AddOrder />
}
AddOrder.js:
function AddOrder() {
const d = new Date();
let text = d.toString();
const { currentUser } = useContext(AuthContext);
const { data, loading, error } = useQuery(queries.GET_USER_BY_ID, {
fetchPolicy: "cache-and-network",
variables: {
id: currentUser.uid
},
});
const [addOrder] = useMutation(queries.ADD_ORDER);
useEffect(() => {
console.log('hi')
})
if(error) {
return <h1> error</h1>;
}
if(loading) {
return <h1> loading</h1>;
}
if (data){
let newCart = []
for(let i=0; i< data.getUser.cart.length; i++){
newCart.push({quantity: data.getUser.cart[i].quantity, _id: data.getUser.cart[i]._id})
}
console.log(newCart)
addOrder({
variables: {
userId: currentUser.uid, status: 'ordered', createdAt: text, products: newCart
}
});
console.log("hello")
}
}
export default AddOrder;
This did not work either. When I reload this it add 3 copies of the same order to the mongodb collection. What is the right way to do this?

TypeError: n.map is not a function

I am trying to return the data from this fetch in some cards in another component, but I get the following error:
TypeError: n.map is not a function.
I guess it's because of the async/await, but I don't know how to fix it.
Thanks a lot
export default function Container(){
const [flights, getNewFlights] = useState({});
const user = sessionStorage.getItem("username");
const tipouser = sessionStorage.getItem("TipoUser");
const APT = sessionStorage.getItem("Base");
const Fecha = sessionStorage.getItem("Fecha");
const fetchFlights = async () => {
try {
const flightsData = await $.ajax({
url: "https://listVuelos.php",
type: 'POST',
data: {
APT,
Fecha
}
})
getNewFlights(JSON.parse(flightsData))
} catch (err) {
console.log("Da error")
}
};
useEffect(() => {
fetchFlights()
const interval = setInterval(() => {
fetchFlights()
}, 100000)
return () => interval
}, []);
return(
<Fragment>
<div className="div_container">
{ flights?.map ( f => <IndexCards data={f}></IndexCards> )}
</div>
</Fragment>
);
}
you can't use map function on an object to overcome the problem you can do something like this:
Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, newValue]))

mapStateToProps is undefined

I want to fetch data from json file and then render it on the screen with React and Redux. JSX is standart, I used the <Provide> tag and set store value to my store. mapStateToProps is going undefined for this.props as well as toTakeData().
Here I have action file with request:
let data = {
loading: true,
items: [],
prevName: null,
selectedProfile: '',
term: ''
}
export function getItems() {
getRequest();
return {
type: 'GET_ITEMS',
payload: data
}
}
const getRequest = async () => {
const response = await fetch('http://localhost:8000/api/item')
.then( response => response.json() )
.then( json => {
data.items = json;
data.selectedProfile = json[0];
data.loading = false;
data.prevName = json[0].general.firstName + ' ' + json[0].general.lastName;
} )
.catch( err => console.error( err ) );
}
And here is component file which suppose to render data:
const mapStateToProps = state => {
console.log(state.items);
return {
items: state.items,
prevName: state.prevName,
selectedProfile: state.selectedProfile,
term: state.term,
loading: state.loading
};
};
const mapActionsToProps = {
toTakeData: getItems
};
export default connect(mapStateToProps, mapActionsToProps)(SelectMenu);

Categories

Resources