React Functional Component update property change - reactjs

I am using react-star package
import ReactStars from "react-rating-stars-component";
It has one issue.
I need to the value to change on state change. But the value is not changing
I am changing the this.state.rating on ajax load.
and setting the value this.rating to be used in submit.
class CallUpdate extends Component<{ match: PropsWithRef<any> }> {
state = {
rating:0
}
rating = 0;
componentDidMount = async () => {
this.id = this.props.match.params.id;
const userCall = await axios.get(`call/show/${this.id}`);
const call: call= userCall.data.data;
this.setState({
rating: call.rating
});
}
submit = async (e: SyntheticEvent) => {
e.preventDefault();
const formData = new FormData();
formData.append('rating', this.rating);
//const formHeaders['Content-Type'] = 'multipart/form-data';
const config = {headers: {'Content-Type' :'multipart/form-data'}};
await axios.post(`call/${this.id}/update`, formData,config);
}
render() {
return (
<ReactStars
count={5}
value={this.state.rating}
onChange={(rating) => {this.rating = rating}}
size={24}
activeColor="#ffd700"
/>
);
}
}
export default CallUpdate;
////
react-starts-component I have added this function. it should be called on props change.
function updateValue(value){
if (value < 0 || value > count) {
setCurrentValue(0);
}
else {
setCurrentValue(value);
}
}
I tried changing the useEffect
useEffect(() => {
addClassNames();
validateInitialValue(props.value, props.count);
setStars(getStars(props.value));
setConfig(props);
createUniqueness();
setIsUsingIcons(iconsUsed(props));
setHalfStarAt(Math.floor(props.value));
setHalfStarHidden(props.isHalf && props.value % 1 < 0.5);
}, []);
to
useEffect(() => {
addClassNames();
validateInitialValue(props.value, props.count);
setStars(getStars(props.value));
setConfig(props);
createUniqueness();
setIsUsingIcons(iconsUsed(props));
setHalfStarAt(Math.floor(props.value));
setHalfStarHidden(props.isHalf && props.value % 1 < 0.5);
}, [props]);
But on state.rating change it is not updating the value.
How do I change the code to make it property change value?

The issue here is you are not updating the state but instead updating a property on class which doesn't cause a re-render and you are using lead whereas you have a call variable in componentDidMount which doesn't exists. You should use a state variable to hold data that might change overtime and reflect in UI.
class CallUpdate extends Component {
state = {
rating: 0
};
componentDidMount = async () => {
const id = this.props.match.params.id;
const userCall = await axios.get(`call/show/${this.id}`);
const call: call = userCall.data.data;
this.setState({
rating: call.rating
});
};
submit = async (e: SyntheticEvent) => {
e.preventDefault();
const formData = new FormData();
formData.append("rating", this.rating);
//const formHeaders['Content-Type'] = 'multipart/form-data';
const config = { headers: { "Content-Type": "multipart/form-data" } };
await axios.post(`call/${this.id}/update`, formData, config);
};
render() {
return (
<ReactStars
count={5}
value={this.state.rating}
onChange={(rating) => this.setState({rating})}
size={24}
activeColor="#ffd700"
/>
);
}
}

I had the same problem when value not updating from useMemo(). You just need to add key prop to ReactStars: key={value}

Related

How to solve a situation when a component calls setState inside useEffect but the dependencies changes on every render?

I have this component:
const updateUrl = (url: string) => history.replaceState(null, '', url);
// TODO: Rename this one to account transactions ATT: #dmuneras
const AccountStatement: FC = () => {
const location = useLocation();
const navigate = useNavigate();
const { virtual_account_number: accountNumber, '*': transactionPath } =
useParams();
const [pagination, setPagination] = useState<PaginatorProps>();
const [goingToInvidualTransaction, setGoingToInvidualTransaction] =
useState<boolean>(false);
const SINGLE_TRANSACTION_PATH_PREFIX = 'transactions/';
// TODO: This one feels fragile, just respecting what I found, but, we could
// investigate if we can jsut rely on the normal routing. ATT. #dmuneras
const transactionId = transactionPath?.replace(
SINGLE_TRANSACTION_PATH_PREFIX,
''
);
const isFirst = useIsFirstRender();
useEffect(() => {
setGoingToInvidualTransaction(!!transactionId);
}, [isFirst]);
const {
state,
queryParams,
dispatch,
reset,
setCursorAfter,
setCursorBefore
} = useLocalState({
cursorAfter: transactionId,
includeCursor: !!transactionId
});
const {
filters,
queryParams: globalQueryParams,
setDateRange
} = useGlobalFilters();
useUpdateEffect(() => {
updateUrl(
`${location.pathname}?${prepareSearchParams(location.search, {
...queryParams,
...globalQueryParams
}).toString()}`
);
}, [transactionId, queryParams]);
useUpdateEffect(() => dispatch(reset()), [globalQueryParams]);
const account_number = accountNumber;
const requestParams = accountsStateToParams({
account_number,
...state,
...filters
});
const { data, isFetching, error, isSuccess } =
useFetchAccountStatementQuery(requestParams);
const virtualAccountTransactions = data && data.data ? data.data : [];
const nextPage = () => {
dispatch(setCursorAfter(data.meta.cursor_next));
};
const prevPage = () => {
dispatch(setCursorBefore(data.meta.cursor_prev));
};
const onRowClick = (_event: React.MouseEvent<HTMLElement>, rowData: any) => {
if (rowData.reference) {
if (rowData.id == transactionId) {
navigate('.');
} else {
const queryParams = prepareSearchParams('', {
reference: rowData.reference,
type: rowData.entry_type,
...globalQueryParams
});
navigate(
`${SINGLE_TRANSACTION_PATH_PREFIX}${rowData.id}?${queryParams}`
);
}
}
};
const checkIfDisabled = (rowData: TransactionData): boolean => {
return !rowData.reference;
};
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
const showTransactionsTable: boolean =
Array.isArray(virtualAccountTransactions) && isSuccess && data?.data;
const onTransactionSourceLoaded = (
transactionSourceData: PayoutDetailData
) => {
const isIncludedInPage: boolean = virtualAccountTransactions.some(
(transaction: TransactionData) => {
if (transactionId) {
return transaction.id === parseInt(transactionId, 10);
}
return false;
}
);
if (!goingToInvidualTransaction || isIncludedInPage) {
return;
}
const fromDate = dayjs(transactionSourceData.timestamp);
const toDate = fromDate.clone().add(30, 'day');
setDateRange({
type: 'custom',
to: toDate.format(dateFormat),
from: fromDate.format(dateFormat)
});
setGoingToInvidualTransaction(false);
};
const fromDate = requestParams.created_after || dayjs().format('YYYY-MM-DD');
const toDate = requestParams.created_before || dayjs().format('YYYY-MM-DD');
const routes = [
{
index: true,
element: (
<BalanceWidget
virtualAccountNumber={account_number}
fromDate={fromDate}
toDate={toDate}
/>
)
},
{
path: `${SINGLE_TRANSACTION_PATH_PREFIX}:transaction_id`,
element: (
<TransactionDetails
onTransactionSourceLoaded={onTransactionSourceLoaded}
/>
)
}
];
return (........
I get this error: Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
The useEffect where the issue is, it is this one:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [data?.meta]);
Considering previous answers, would the solution be to make sure I return a new object each time? But I am not sure what would be the best approach. Any clues ?
did you want the useEffect to start every changes of 'data?.meta' ?
Without reading all the code, I believe the data.meta object changes on every render. There is a way to change the useEffect to narrow done its execution conditions:
useEffect(() => {
if (data?.meta) {
setPagination({
showPrev: data.meta.has_previous_page,
showNext: data.meta.has_next_page
});
}
}, [!data?.meta, data?.meta?.has_previous_page, data?.meta?.has_next_page]);
Please note the ! before data.?.meta which makes the hook test only for presence or absence of the object, since your code doesn't need more than that information.

Cannot setstate in nested axios post request in react

I am trying to access the res.data.id from a nested axios.post call and assign it to 'activeId' variable. I am calling the handleSaveAll() function on a button Click event. When the button is clicked, When I console the 'res.data.Id', its returning the value properly, but when I console the 'activeId', it's returning null, which means the 'res.data.id' cannot be assigned. Does anyone have a solution? Thanks in advance
const [activeId, setActiveId] = useState(null);
useEffect(() => {}, [activeId]);
const save1 = () => {
axios.get(api1, getDefaultHeaders())
.then(() => {
const data = {item1: item1,};
axios.post(api2, data, getDefaultHeaders()).then((res) => {
setActiveId(res.data.id);
console.log(res.data.id); // result: e.g. 10
});
});
};
const save2 = () => {
console.log(activeId); // result: null
};
const handleSaveAll = () => {
save1();
save2();
console.log(activeId); // result: again its still null
};
return (
<button type='submit' onClick={handleSaveAll}>Save</button>
);
Setting the state in React acts like an async function.
Meaning that the when you set the state and put a console.log right after it, like in your example, the console.log function runs before the state has actually finished updating.
Which is why we have useEffect, a built-in React hook that activates a callback when one of it's dependencies have changed.
Example:
useEffect(() => {
console.log(activeId);
}, [activeId);
The callback will run every time the state value changes and only after it has finished changing and a render has occurred.
Edit:
Based on the discussion in the comments.
const handleSaveSections = () => {
// ... Your logic with the `setState` at the end.
}
useEffect(() => {
if (activeId === null) {
return;
}
save2(); // ( or any other function / logic you need )
}, [activeId]);
return (
<button onClick={handleSaveSections}>Click me!</button>
)
As the setState is a async task, you will not see the changes directly.
If you want to see the changes after the axios call, you can use the following code :
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
setTimeout(()=>console.log(activeId),0);
})
useEffect(() => {
}, [activeId]);
const [activeId, setActiveId] = useState(null);
const save1 = () => {
const handleSaveSections = async () => {
activeMetric &&
axios.get(api1, getDefaultHeaders()).then(res => {
if (res.data.length > 0) {
Swal.fire({
text: 'Record already exists',
icon: 'error',
});
return false;
}
else {
const data = {
item1: item1,
item2: item2
}
axios.post(api2, data, getDefaultHeaders())
.then((res) => {
setActiveId(res.data.id)
console.log(res.data.id) // result: e.g. 10
})
}
});
}
handleSaveSections()
}
const save2 = () => {
console.log(activeId); //correct result would be shown here
}
const handleSaveAll = () => {
save1();
save2();
}
return (
<button type="submit" onClick={handleSaveAll}>Save</button>
)

Redux - Fetch data, but render in another component

I'm currently fetching data in Component1, then dispatching an action to update the store with the response. The data can be seen in Component2 in this.props, but how can I render it when the response is returned? I need a way to reload the component when the data comes back.
Initially I had a series of functions run in componentDidMount but those are all executed before the data is returned to the Redux store from Component1. Is there some sort of async/await style between components?
class Component1 extends React.Component {
componentDidMount() {
this.retrieveData()
}
retrieveData = async () => {
let res = await axios.get('url')
updateParam(res.data) // Redux action creator
}
}
class Component2 extends React.Component {
componentDidMount() {
this.sortData()
}
sortData = props => {
const { param } = this.props
let result = param.sort((a,b) => a - b)
}
}
mapStateToProps = state => {
return { param: state.param }
}
connect(mapStateToProps)(Component2)
In Component2, this.props is undefined initially because the data has not yet returned. By the time it is returned, the component will not rerender despite this.props being populated with data.
Assuming updateParam action creator is correctly wrapped in call to dispatch in mapDispatchToProps in the connect HOC AND properly accessed from props in Component1, then I suggest checking/comparing props with previous props in componentDidUpdate and calling sortData if specifically the param prop value updated.
class Component2 extends React.Component {
componentDidMount() {
this.sortData()
}
componentDidUpdate(prevProps) {
const { param } = this.props;
if (prevProps.param !== param) { // <-- if param prop updated, sort
this.sortData();
}
}
sortData = () => {
const { param } = this.props
let result = param.sort((a, b) => a - b));
// do something with result
}
}
mapStateToProps = state => ({
param: state.param,
});
connect(mapStateToProps)(Component2);
EDIT
Given component code from repository
let appointmentDates: object = {};
class Appointments extends React.Component<ApptProps> {
componentDidUpdate(prevProps: any) {
if (prevProps.apptList !== this.props.apptList) {
appointmentDates = {};
this.setAppointmentDates();
this.sortAppointmentsByDate();
this.forceUpdate();
}
}
setAppointmentDates = () => {
const { date } = this.props;
for (let i = 0; i < 5; i++) {
const d = new Date(
new Date(date).setDate(new Date(date).getDate() + i)
);
let month = new Date(d).toLocaleString("default", {
month: "long"
});
let dateOfMonth = new Date(d).getDate();
let dayOfWeek = new Date(d).toLocaleString("default", {
weekday: "short"
});
// #ts-ignore
appointmentDates[dayOfWeek + ". " + month + " " + dateOfMonth] = [];
}
};
sortAppointmentsByDate = () => {
const { apptList } = this.props;
let dates: string[] = [];
dates = Object.keys(appointmentDates);
apptList.map((appt: AppointmentQuery) => {
return dates.map(date => {
if (
new Date(appt.appointmentTime).getDate().toString() ===
// #ts-ignore
date.match(/\d+/)[0]
) {
// #ts-ignore
appointmentDates[date].push(appt);
}
return null;
});
});
};
render() {
let list: any = appointmentDates;
return (
<section id="appointmentContainer">
{Object.keys(appointmentDates).map(date => {
return (
<div className="appointmentDateColumn" key={date}>
<span className="appointmentDate">{date}</span>
{list[date].map(
(apptInfo: AppointmentQuery, i: number) => {
return (
<AppointmentCard
key={i}
apptInfo={apptInfo}
/>
);
}
)}
</div>
);
})}
</section>
);
}
}
appointmentDates should really be a local component state object, then when you update it in a lifecycle function react will correctly rerender and you won't need to force anything. OR since you aren't doing anything other than computing formatted data to render, Appointments should just call setAppointmentDates and sortAppointmentsByDate in the render function.

React Native hooks, useRef and useEffect

I'm trying to translate a Class Component into a Functional one with React Native.
My Search component lets the user search for a film name and I'm making an API call to show him all corresponding films.
Here is my class component :
class Search extends React.Component {
constructor(props) {
super(props);
this.searchedText = "";
this.page = 0
this.totalPages = 0
this.state = {
films: [],
isLoading: false
}
}
_loadFilms() {
if (this.searchedText.length > 0) {
this.setState({ isLoading: true })
getFilmsFromApiWithSearchedText(this.searchedText, this.page+1).then(data => {
this.page = data.page
this.totalPages = data.total_pages
this.setState({
films: [ ...this.state.films, ...data.results ],
isLoading: false
})
})
}
}
_searchTextInputChanged(text) {
this.searchedText = text
}
_searchFilms() {
this.page = 0
this.totalPages = 0
this.setState({
films: [],
}, () => {
this._loadFilms()
})
}
render() {
return (
<View style={styles.main_container}>
<TextInput
style={styles.textinput}
placeholder='Titre du film'
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._searchFilms()}
/>
<Button title='Rechercher' onPress={() => this._searchFilms()}/>
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.page < this.totalPages) {
this._loadFilms()
}
}}
/>
</View>
)
}
}
render() {
return (
<View>
<TextInput
onChangeText={(text) => this._searchTextInputChanged(text)}
onSubmitEditing={() => this._searchFilms()}
/>
<Button title='Search' onPress={() => this._searchFilms()}/>
<FlatList
data={this.state.films}
keyExtractor={(item) => item.id.toString()}
renderItem={({item}) => <FilmItem film={item}/>}
onEndReachedThreshold={0.5}
onEndReached={() => {
if (this.page < this.totalPages) {
this._loadFilms()
}
}}
/>
{this._displayLoading()}
</View>
)
}
}
How can I translate the following with hooks :
this.page and this.totalPages ? is useRef the solution ?
in _searchFilms() I'm using setState callback to make a new API call when my film list is empty (because it's a new search). But doing it right after doesn't work because setState is asynchronous.
But I can't find a way to do it with hooks.
I think useEffect could do this but :
I only want to make this API call when my film list is empty, because I call _searchFilms() for a new search.
_loadFilms() is called on user scroll to add more films to the FlatList (for the same search) so I can't clear this.films in this case.
Here is how I translated it so far :
const Search = () => {
const [searchText, setSearchText] = useState('');
const [films, setFilms] = useState([]);
// handle pagination
const page = useRef(0);
const totalPages = useRef(0);
// handle api fetch
const [isLoading, setIsLoading] = useState(false);
const loadFilmsFromApi = () => {
getFilmsFromApiWithSearchedText(searchText, page + 1).then((data) => {
page.current = data.page;
totalPages.current = data.total_pages;
setFilms(films => [...films, ...data.results]);
setIsLoading(false);
})
};
const searchFilm = () => {
if (searchText.length > 0) {
page.current = 0;
totalPages.current = 0;
setFilms([]);
// HERE MY Films list won't be cleared (setState asynchronous)
loadFilmsFromApi();
// after the api call, clear input
setSearchText('');
}
};
useEffect(() => {
console.log(page, totalPages, "Film number" + films.length);
}, [films]);
I think you are on the right path. As for totalPages and page, having it as a ref makes sense if you want to maintain that values between different renders ( when setting state )
const Search = () => {
const [searchText, setSearchText] = useState('');
const [films, setFilms] = useState([]);
// handle pagination
const page = useRef(0);
const totalPages = useRef(0);
// handle api fetch
const [isLoading, setIsLoading] = useState(false);
// This can be invoked by either search or user scroll
// When pageNum is undefined, it means it is triggered by search
const loadFilmsFromApi = (pageNum) => {
console.log("APPEL", 'loadFills');
getFilmsFromApiWithSearchedText(searchText, pageNum ? pageNum + 1 : 1).then((data) => {
page.current = data.page;
totalPages.current = data.total_pages;
setFilms(films => {
if(pageNum) {
return [...films, ...data.results];
} else {
return [data.results];
}
});
setIsLoading(false);
})
};
useEffect(() => {
if (searchText.length > 0) {
page.current = 0;
totalPages.current = 0;
setFilms([]);
loadFilmsFromApi();
// after the api call, clear input
setSearchText('');
}
}, [searchText, loadFilmsFromApi]);
useEffect(() => {
console.log(page, totalPages, "Nombre de film " + films.length);
}, [films]);
return ( <
div > Search < /div>
);
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
Not totally clear what your question is, but it sounds like you want to clear films state before you fire off the query to the api? I am also not clear on the use of useRef here - useRef is simply a way to get a reference to an element so it's easy to access it later - like get a reference to a div and be able to access it easily via myDivRef.current
const = myDivRef = useRef;
...
<div ref={myDivRef}/>
If that is the case, then I would simply set the state of films once in the return of the API call. WRT to the refs, it seems like you this should just be normal variables, or possible state items in your function.
UPDATE:
After clearing up the goal here, you could simply add a parameter to loadFilmsFromApi to determine if you should append or overwrite:
const loadFilmsFromApi = (append) => {
getFilmsFromApiWithSearchedText(searchText, page + 1).then((data) => {
page.current = data.page;
totalPages.current = data.total_pages;
if (append) {
setFilms({
films: this.state.films.concat(data.results)
});
} else {
setFilms({
films: data.results
});
}
setIsLoading(false);
})
};

how to handle race conditions in class components?

Suppose there is a component where ask server to do some search and response will be rendered. How to ensure most recent request's response is rendered even if server side for any reason answers in different ordering? I'm not asking about cancelling previous request since it's not always possible with reasonable efforts.
onClick = () => {
apiCall(this.state.searchQuery).then(items => this.setState({ items }));
};
Is there elegant way to handle that? By now I know few approaches:
disabling button till request comes(provides bad experiences in large amount of cases - say for searching while typing)
checking inside then() if request's params matches this.props/this.state data(does not handle case when we intentionally forced new search with same query - say by pressing Enter/clicking "Search" button)
onClick = () => {
const searchQuery = this.state.searchQuery;
apiCall(searchQuery)
.then(items =>
this.state.searchQuery === searchQuery
&& this.setState({ items })
);
};
marking requests somehow and checking if it's latest(works, but looks too verboose especially if there are few requests we need to check)
searchQueryIndex = 0;
onClick = () => {
this.searchQueryIndex++;
const index = this.searchQueryIndex;
apiCall(this.state.searchQuery)
.then(items =>
this.searchQueryIndex === searchQueryIndex
&& this.setState({ items })
);
};
I'd call that trio "ugly, broken and messy".
Is there something such clear way as hooks allow:
useEffect(() => {
const isCanceled = false;
apiCall(searchQuery).then(items => !isCanceled && setItems(items));
return () => {isCanceled = true;};
}, [searchQuery])
Your onClick handler suggest a class component since you use this and this.setState:
onClick = () => {
apiCall(this.state.searchQuery).then(items =>
this.setState({ items })
);
};
I adjusted onlyLastRequestedPromise to take a function that will return something (you can return Promise.reject('cancelled') or anything).
const onlyLastRequestedPromise = (promiseIds => {
const whenResolve = (
promise,
id,
promiseID,
resolveValue,
whenCancelled = () => Promise.reject('cancelled')
) => {
if (promise !== undefined) {
//called by user adding a promise
promiseIds[id] = {};
} else {
//called because promise is resolved
return promiseID === promiseIds[id]
? Promise.resolve(resolveValue)
: whenCancelled(resolveValue);
}
return (function(currentPromiseID) {
return promise.then(function(result) {
return whenResolve(
undefined,
id,
currentPromiseID,
result
);
});
})(promiseIds[id]);
};
return (id = 'general', whenCancelled) => promise =>
whenResolve(
promise,
id,
undefined,
undefined,
whenCancelled
);
})({});
A class example on how to use it:
class Component extends React.Component {
CANCELLED = {};
last = onlyLastRequestedPromise(
'search',
() => this.CANCELLED
);
onSearch = () => {
this.last(apiCall(this.state.searchQuery)).then(
items =>
items !== this.CANCELLED && this.setState({ items })
);
};
changeAndSearch = e => {
this.setState(
{}, //state with new value
() => this.onSearch() //onSearch after state update
);
};
render() {
return (
<div>
<SearchButton onClick={this.onSearch} />
<Other onChange={this.changeAndSearch} />
</div>
);
}
}
I agree it's a lot of code but since you put most of the implementation in the lib it should not clutter your components.
If you had a functional component you could create the last function with useRef:
//
function ComponentContainer(props) {
const CANCELLED = useRef({});
const last = useRef(
onlyLastRequestedPromise('search', () => CANCELLED)
);
const [searchQuery,setSearchQuery] = useState({});
const mounted = useIsMounted();
const onSearch = useCallback(
last(apiCall(searchQuery)).then(
items =>
items !== CANCELLED &&
mounted.current &&
//do something with items
)
);
}
Finally figured out how to utilize closure to mimic "just ignore that" approach from hooks' world:
class MyComponent extends React.Component {
const ignorePrevRequest = () => {}; // empty function by default
loadSomeData() {
this.ignorePrevRequest();
let cancelled = false;
this.ignorePrevRequest = () => { cancelled = true; }; // closure comes to play
doSomeCall().then(data => !cancelled && this.setState({ data }))
}
}

Resources