debounce async/await and update component state - reactjs

I have question about debounce async function. Why my response is undefined? validatePrice is ajax call and I receive response from server and return it (it is defined for sure).
I would like to make ajax call after user stops writing and update state after I get reponse. Am I doing it right way?
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
this.setState(state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}), () => this.doPriceValidation());
}
doPriceValidation = debounce(async () => {
const response = await this.props.validatePrice(this.state.selected);
console.log(response);
//this.setState({ selected: res.TOE });
}, 400);
actions.js
export function validatePrice(product) {
const actionUrl = new Localization().getURL(baseUrl, 'ValidateTargetPrice');
return function (dispatch) {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST });
dispatch(showLoader());
return axios.post(actionUrl, { argModel: product }, { headers })
.then((res) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_FULFILLED, payload: res.data });
console.log(res.data); // here response is OK (defined)
return res;
})
.catch((err) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_REJECTED, payload: err.message });
})
.then((res) => {
dispatch(hideLoader());
return res.data;
});
};
}

Please find below the working code with lodash debounce function.
Also here is the codesandbox link to play with.
Some changes:-
1) I have defined validatePrice in same component instead of taking from prop.
2) Defined the debounce function in componentDidMount.
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class App extends React.Component {
state = {
selected: { Price: 10 }
};
componentDidMount() {
this.search = _.debounce(async () => {
const response = await this.validatePrice(this.state.selected);
console.log(response);
}, 2000);
}
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
console.log(value);
this.setState(
state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}),
() => this.doPriceValidation()
);
};
doPriceValidation = () => {
this.search();
};
validatePrice = selected => {
return new Promise(resolve => resolve(`response sent ${selected}`));
};
render() {
return (
<div className="App">
<input type="text" onChange={this.handleTargetPriceDayChange} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope that helps!!!

You can use the throttle-debounce library to achieve your goal.
Import code in top
import { debounce } from 'throttle-debounce';
Define below code in constructor
// Here I have consider 'doPriceValidationFunc' is the async function
this.doPriceValidation = debounce(400, this.doPriceValidationFunc);
That's it.

Related

Vitest React - component not "re-rendering" after server request

This is my simplifiedReact component:
export const EntryDetail = () => {
const { articleId } = useParams();
const [article, setArticle] = useState({ title: null, body: null, comments: [], likes: [] });
const { title, body, comments, likes } = article;
useEffect(() => {
(async () => {
try {
const response = await getArticleDetail(articleId);
const { title, body, comments, likes } = response.data;
setArticle({ title, body, comments, likes });
} catch (e) {
console.error(e);
}
})();
}, []);
return (
<Container>
{
!article.title
? <div>Loading...</div>
: <>
<h1>{title}</h1>
<p className="body">{body}</p>
</>
}
</Container>
);
};
And this is my test:
import { render, screen } from '#testing-library/react';
import { StateProvider } from '../../config/state';
import { EntryDetail } from './index';
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
vi.mock('react-router-dom', () => ({
useParams: () => ({
articleId: '63d466ca3d00b50db15aed93',
}),
}));
describe("EntryDetail component", () => {
it("should render the EntryDetail component correctly", async () => {
render(
<EntryDetail />
);
await flushPromises();
const element = screen.getByRole("heading");
expect(element).toBeInTheDocument();
});
});
This is what I'm getting in the console:
I was expecting the "await flushPromises()" would actually wait for the response from the call in the useEffect to the "update" the component", but I guess this is kind of "static"? How should this be handled? I actually want to test if the component itself works effectively, I don't want to mock a response, I want to see if the component actually reacts appropriately after the response is back.

PATCH request seems like a step behind

Hey folks really hope someone can help me here. I'm successfully updating my object in my mongo cluster, it updates but it does not render that update straight away to the browser. It will only update after a reload or when I run my update function again, it doesn't fetch that update straight away and I can't understand why. Does anyone have any suggestions?
I'm using context and reducer.
PlantDetails
import { usePlantsContext } from "../hooks/usePlantsContext";
import formatDistanceToNow from "date-fns/formatDistanceToNow";
import { useState } from "react";
import CalendarComponent from "./CalendarComponent";
const PlantDetails = ({ plant }) => {
const [watered, setWatered] = useState(false)
const [newWaterDate, setNewWaterDate] = useState("")
const { dispatch } = usePlantsContext();
const handleClick = async () => {
const response = await fetch("/api/plants/" + plant._id, {
method: "DELETE",
});
const json = await response.json();
if (response.ok) {
dispatch({ type: "DELETE_PLANT", payload: json });
}
};
const updatePlant = async (e) => {
e.preventDefault()
plant.nextWaterDate = newWaterDate
const response = await fetch("api/plants/" + plant._id, {
method: "PATCH",
body: JSON.stringify(plant),
headers: {
'Content-Type': 'application/json'
}
})
const json = await response.json()
if(response.ok) {
dispatch({ type: "UPDATE_PLANT", payload: json })
}
console.log('updated')
setWatered(false)
}
return (
<div className="plant-details">
<h4>{plant.plantName}</h4>
<p>{plant.quickInfo}</p>
<p>
{formatDistanceToNow(new Date(plant.createdAt), { addSuffix: true })}
</p>
<span onClick={handleClick}>delete</span>
<div>
<p>next water date: {plant.nextWaterDate}</p>
<input onChange={(e) => setNewWaterDate(e.target.value)}/>
<button onClick={updatePlant}>update</button>
<input value={watered} type="checkbox" id="toWater" onChange={() => setWatered(true)}/>
<label for="toWater">watered</label>
{watered && <CalendarComponent updatePlant={updatePlant} setNextWaterDate={setNewWaterDate}/>}
</div>
</div>
);
};
export default PlantDetails;
Context which wraps my
import { createContext, useReducer } from 'react'
export const PlantsContext = createContext()
export const plantsReducer = (state, action) => {
switch(action.type) {
case 'SET_PLANTS':
return {
plants: action.payload
}
case 'CREATE_PLANT':
return {
plants: [action.payload, ...state.plants]
}
case 'DELETE_PLANT':
return {
plants: state.plants.filter((p) => p._id !== action.payload._id)
}
case 'UPDATE_PLANT':
return {
plants: state.plants.map((p) => p._id === action.payload._id ? action.payload : p )
}
default:
return state
}
}
export const PlantsContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(plantsReducer, {
plants: null
})
return (
<PlantsContext.Provider value={{...state, dispatch}}>
{ children }
</PlantsContext.Provider>
)
}
My plantController (update)
const updatePlant = async (req, res) => {
const { id } = req.params
if(!mongoose.Types.ObjectId.isValid(id)) {
return res.status(404).json({ error: "No plant" })
}
const plant = await Plant.findByIdAndUpdate({ _id: id }, {
...req.body
})
if (!plant) {
return res.status(400).json({ error: "No plant" })
}
res.status(200)
.json(plant)
}
Home component
import { useEffect } from "react";
import PlantDetails from "../components/PlantDetails";
import PlantForm from "../components/PlantForm";
import CalendarComponent from "../components/CalendarComponent";
import { usePlantsContext } from "../hooks/usePlantsContext";
const Home = () => {
const { plants, dispatch } = usePlantsContext();
useEffect(() => {
const fetchPlants = async () => {
console.log("called");
// ONLY FOR DEVELOPMENT!
const response = await fetch("/api/plants");
const json = await response.json();
if (response.ok) {
dispatch({ type: "SET_PLANTS", payload: json });
}
};
fetchPlants();
}, [dispatch]);
return (
<div className="home">
<div className="plants">
{plants &&
plants.map((plant) => <PlantDetails key={plant._id} plant={plant} />)}
</div>
<PlantForm />
</div>
);
};
export default Home;
Any help would be greatly appreciated.
My patch requests were going through smoothly but my state would not update until I reloaded my page. It was not returning the document after the update was applied.
https://mongoosejs.com/docs/tutorials/findoneandupdate.html#:~:text=%3B%20//%2059-,You,-should%20set%20the

react-testing-library and MSW

I have been trying to implement some tests on my project, but I got some blockers.
This error:
Unable to find an element with the text: C-3PO/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
I don't know how to fix it. Should I use another query or I am missing something?
import { render, screen } from "#testing-library/react";
import CharacterList from "./index";
import { rest } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
rest.get("https://swapi.dev/api/people/", (req, res, ctx) => {
return res(
ctx.json({ results: [{ name: "Luke Skywalker", gender: "male" }] })
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("render characters", () => {
it("should render character C-3PO when get api response", async () => {
render(<CharacterList />);
const character = await screen.findByText("C-3PO/i");
expect(character).toBeInTheDocument();
});
});
and my component:
import { NavLink } from "react-router-dom";
import { useEffect, useState } from "react";
export default function CharactersList() {
const [data, setData] = useState(undefined);
const [home, setHome] = useState(undefined);
useEffect(() => {
fetch("https://swapi.dev/api/people/")
.then((response) => {
if (response) {
return response.json();
} else {
return Promise.reject(response);
}
})
.then((data) => {
setData(data);
});
}, []);
if (data) {
return data.results.map((item) => {
const id = item.url.slice(29);
return (
<>
<NavLink to={`/character/${id}`}>{item.name}</NavLink>
<p>Gender: {item.gender}</p>
<p>Home planet: {item.homeworld}</p>
</>
);
});
} else {
return <p>Loading...</p>;
}
}
Please Add screen.debug() to see your actual screen, from that you can consider which get method will work to you.
I think the problem is you don't have C-3PO/i text in the DOM
describe("render characters", () => {
it("should render character C-3PO when get api response", async () => {
render(<CharacterList />);
screen.debug(); <------- ADD THIS
const character = await screen.findByText("C-3PO/i");
expect(character).toBeInTheDocument();
});
});

Jest Mocked data not loaded during test

I have a component that loads data from an API which I mocked for my test but it is not loaded as the test cannot find the element which contain the data.
component:
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect, useCallback } from "react";
import { businessDataActions } from "../store/business-data";
import { fetchBusinessListing } from "../services/business-listing";
import styles from "../styles/BizCard.module.css";
import BizCardItem from "./BizCardItem";
const BizCard = (props) => {
const dispatch = useDispatch();
const [listing, setListing] = useState([]);
//load all listing
const fetchListing = useCallback(async () => {
dispatch(businessDataActions.setIsLoading({ isLoading: true }));
const ListingService = await fetchBusinessListing();
if (ListingService.success) {
setListing(ListingService.data);
} else {
dispatch(
businessDataActions.setNotify({
severity: "error",
message: "Problem when fetching listing.",
state: true,
})
);
}
dispatch(businessDataActions.setIsLoading({ isLoading: false }));
}, []);
useEffect(() => {
fetchListing();
}, []);
const businessList = listing.map((item) => (
<BizCardItem
key={item.key}
id={item.id}
name={item.name}
shortDescription={item.shortDescription}
imageUrl={item.imageUrl}
/>
));
return (
<div className={styles.grid} role="grid">
{businessList}
</div>
);
};
test file:
const bizListing = [
...some fake data
];
jest.mock("../../services/business-listing", () => {
return function fakeListing() {
return { success: true, data: bizListing };
}
});
afterEach(cleanup);
describe('BizCard', () => {
test("loading listing", async () => {
useSession.mockReturnValueOnce([null, false]);
await act(async () => {render(
<BizCard />
)});
const itemGrid = await screen.findAllByRole("gridcell");
expect(itemGrid).not.toHaveLength(0);
});
});
services/business-listing:
export const fetchBusinessListing = async() => {
try {
const response = await fetch(
"/api/business"
);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const data = await response.json();
const loadedBusiness = [];
for (const key in data) {
let imgUrl =
data[key].imageUrl !== "undefined" && data[key].imageUrl !== ""
? data[key].imageUrl
: '/no-image.png';
loadedBusiness.push({
key: data[key]._id,
id: data[key]._id,
name: data[key].businessName,
shortDescription: data[key].shortDescription,
imageUrl: imgUrl,
});
}
return { success: true, data: loadedBusiness };
} catch (error) {
return ({success: false, message: error.message});
}
}
The test executed with these returned:
TypeError: (0 , _businessListing.fetchBusinessListing) is not a function
48 | // }
49 |
> 50 | const ListingService = await fetchBusinessListing();
Unable to find role="gridcell"
I can confirm gridcell is rendered when I am using browser.
Can anyone please shed some light on my problem
Manage to solve the problem myself, problem is with the mock:
jest.mock("../../services/business-listing", () => {
return {
fetchBusinessListing: jest.fn(() => { return { success: true, data: bizListing }}),
}
});

Api call not firing up in redux action with debouce

Im trying to implement debouncing with my own pure js function for calling the action in action file with axios POST request.
The below code in the input box component
import React, { useState, useCallback } from 'react'
import { searchDrug } from '../../actions/drug-interaction'
function CustomInputSearch(props) {
const { handleSelectInput } = props
const apiCall = (value) => {
searchDrug(value)
}
const debounce = (apiFunc, delay) => {
let inDebounce
return function () {
const context = this
const args = arguments
clearTimeout(inDebounce)
inDebounce = setTimeout(() => apiFunc.apply(context, args), delay)
}
}
const optimizedVersion = debounce(apiCall, 500)
const handleChange = (e) => {
optimizedVersion(e.target.value)
}
return (
<div>
<input
className='form-control'
placeholder='Search Drug...'
onKeyUp={handleChange}
/>
</div>
)
}
export default CustomInputSearch
Ignore the unnececssary imports.
The below code is the action file.
export const searchDrug = (drug) => {
const params = {
"start": drug,
"limit": 100
}
let axiosConfig = {
headers: {
// 'Access-Control-Allow-Origin': '*'
}
}
return (dispatch) => {
dispatch({ type: 'DRUG_LIST_NOTIFY', payload: { drugListLoading: true } })
axios.post(`${API_URL}/drug/autocomplete`, params, axiosConfig)
.then((response) => {
dispatch({
type: 'DRUG_LIST',
payload: { response: response.data, drugListLoading: false }
})
})
.catch((error) => {
dispatch({ type: 'DRUG_LIST_NOTIFY', payload: { drugListLoading: false } })
if (error.response && error.response.status === 401) {
window.open('/?src=auth-error', '_self')
}
});
};
}
But im not seeing any request going in network tab in browser.Im also composedWithDevtools in redux store.Thanks in advance
It is because your searchDrug action must be came from dispatch instead of calling it directly.
import React, { useState, useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { searchDrug } from '../../actions/drug-interaction'
function CustomInputSearch(props) {
const { handleSelectInput } = props
const dispatch = useDispatch()
const apiCall = (value) => {
dispatch(searchDrug(value))
}
...

Resources