I'm using react-native-vector-icons, and I want to change the icon when I press the button.
const {favorites, toggleFavorite} = useFavorites();
const isFavorite = favorites.includes(value);
return (
...
<Button
icon={
<Icon
name={isFavorite ? 'favorite' : 'favorite-border'}
size={24}
color={theme.colors.text.contrast}
/>
}
onPress={() => toggleFavorite(value)}
type="clear"
/>
export const useFavorites = (): {
favorites: string[];
toggleFavorite: (lineNumber: string) => Promise<void>;
} => {
const [favorites, setFavorites] = useState<string[]>([]);
const readFavorites = async () => {
try {
const value = await AsyncStorage.getItem(FAVORITES_KEY);
setFavorites(value ? value.split(',') : []);
} catch (e) {
console.log(e);
}
};
const toggleFavorite = async (lineNumber: string) => {
let newFavorites = favorites;
if (favorites.includes(lineNumber)) {
newFavorites = newFavorites.filter(favorite => favorite !== lineNumber);
} else {
newFavorites.push(lineNumber);
}
try {
await AsyncStorage.setItem(FAVORITES_KEY, newFavorites.join(','));
setFavorites(newFavorites);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
readFavorites();
}, []);
return {
favorites,
toggleFavorite,
};
};
When I press the button, the value of isFavorite is toggled correctly. Also it works when isFavorite is true initially. It doesn't work the other way around. What am I missing here?
EDIT: Added useFavorites for more context
Your problem is you add item wrong way. You cannot mutate state directly. Just update like this:
if (favorites.includes(lineNumber)) {
newFavorites = newFavorites.filter(favorite => favorite !== lineNumber);
} else {
newFavorites = [...favorites, lineNumber]
}
Related
How can I implement native javascript style confirm modal using context api?
I made a codesandbox.
https://codesandbox.io/s/little-sunset-806rdh?file=/src/App.js
Or please see below code.
context.js
const confirmContext = createContext()
function ConfirmProvider({children}){
const [visible, setVisible] = useState(false)
const [message, setMessage] = useState('')
const open = (message) => {
setVisible(true)
setMessage(message)
}
const handleSubmitClick = () => { }
const handleCancelClick = () => { }
if (!visible) return <div />
return (
<ConfirmContext.Provider value={{ open }}>
<div>
{message}
<button onClick={handleSubmitClick}>OK</button>
<button onClick={handleCancelClick}>Cancel</button>
</div>
{children}
</ConfirmContext.Provider>
)
}
const useConfirm = () => useContext(ConfirmContext)
export { ConfirmProvider }
delete.js
function Delete(){
const confirm = useConfirm()
const handleClick = () => {
confirm.open('Are you really delete it?')
// How do I code here when 'OK' or 'Cancel' Button Click?
// if(confirm.open()){
// // do something
// } else {
// // do nothing
// }
}
return (
<div>
<button onClick={handleClick}>Delete</button>
</div>
)
}
return Promise in open function will implement this pattern.
const open = ({ message, confirmText, cancelText }) => {
setVisible(true)
setMessage(message)
return new Promise((res, rej) => {
resolveCallback = res
})
}
const onConfirmClick = () => {
window.alert('on confirm click')
resolveCallback(true)
setVisible(false)
}
const onCancelClick = () => {
window.alert('on cancel click')
resolveCallback(false)
setVisible(false)
}
// use
const c = confirm.open('you really delete it?')
if(c) // do something
I'm new to react but I've encountered enough errors to know not to call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks
I'm trying to import data from mongodb atlas into my app.
From the codes written below, I don't think it has violated the rule. Any help would be great thx!
import { useState, useEffect } from "react";
import axios from "axios";
const Events = () => {
const [allEvents, setEvents] = useState([]);
const fetchEvents = async () => {
try {
const { data } = await axios.get(`http://localhost:5000/get/events`);
return data
} catch (err) {
console.log(err);
}
};
useEffect(() => {
fetchEvents();
}, []);
};
export default Events;
function CalendarComponent() {
//useState declarations
const [newEvent, setNewEvent] = useState({
id: "",
title: "",
start: "",
action: "",
});
const [allEvents, setEvents] = useState(events);
const [toggleAdd, setToggleAdd] = useState(false);
const [msg, setMsg] = useState("");
//handle creating new event on button click
function handleAddSlot() {
const title = newEvent.title;
const start = newEvent.start;
const end = start;
let success = 0;
if (title && newEvent.action === control.ADD) {
// add new event into the list
const id = allEvents.length;
if(data.Events.some(i=>i["Serial Number"]===title)){
// let newArr = [...data.Events]
// const res = newArr.find(e=>e["Serial Number"]===title)["Scheduled Sample Date"]
// console.log(res);
// res.push({id:res.length,start,end})
data.Events.map(item=>item["Serial Number"]===title?{...item}["Scheduled Sample Date"].push({"id":{...item}["Scheduled Sample Date"].length,start}):item)
}
else{
setEvents((prev) => [...prev, { start, end, title }]);
}
setToggleAdd(true);
console.log(data.Events)
success = 1;
}
if (newEvent.action === control.UPDATE) {
// find id of existing event to update
setEvents((currEvents) =>
currEvents.map((event) => {
if (event.id === newEvent.id) {
return {
...event,
start: start,
end: start,
title: title,
};
} else {
return event;
}
})
);
success = 1;
console.log(allEvents);
}
// if action successful, close modal and clear all fields
if (success === 1) {
setToggleAdd(false);
clearFields();
}
}
// handle when user select existing scope event
function handleSelectEvent(event) {
setMsg("Update Scope");
const start = new Date(event.start);
setNewEvent({
...newEvent,
action: control.UPDATE,
title: event.title,
start,
id: event.id,
});
setToggleAdd(true);
}
//handle calendar slot click
function handleClick(e) {
newEvent.start = e.start;
newEvent.action = control.ADD;
setMsg("Add New Scope");
if (
// ? Can be better written
document.getElementById("id01")?.classList.contains("setDisplay") === true
) {
setToggleAdd(true);
}
}
//handle modal close when user clicks on x
function toggleClick() {
if (toggleAdd === true) {
setToggleAdd(false);
clearFields();
}
}
//function to clear all fields
function clearFields() {
newEvent.start = "";
newEvent.title = "";
}
return (
<div className={`calendar-container`}>
<AddModal
id="id01"
toggle={toggleAdd}
newEvent={newEvent}
toggleClick={toggleClick}
handleSelectSlot={handleAddSlot}
setNewEvent={setNewEvent}
msg={msg}
/>
<Calendar
localizer={localizer}
events={allEvents}
popup
defaultView="month"
longPressThreshold={1}
views={["month"]}
eventPropGetter={(event) => {
const identifier = data.Events.find(
(item) => item["Type"] === event.type
);
const backgroundColor = identifier
? data.Color.find((item) => item[`${identifier.Type}`])[
`${identifier.Type}`
]
: "";
return { style: { backgroundColor } };
}}
onSelectEvent={handleSelectEvent}
onSelectSlot={(e) => handleClick(e)}
selectable
style={{ height: 500, margin: "30px" }}
components={{
toolbar: CustomToolbar,
}}
/>
<div className="calendar-sidebar">
<Button text="Add New" button="button" onClick={handleClick} />
<Log type='Schedule' />
</div>
</div>
);
}
I've attached the CalendarComponent. I can't really seem to find the problem in CalendarComponent myself. Hopefully it's not in the react big calendar package and I'm not missing something
I am working on a react app where I have a userSettings screen for the user to update their settings on clicking a save button. I have two sliding switches that are saved and a dispatch function is ran to post the data.
Each switch has their own toggle function, and all the functions run at the same time.
My problem is that when I pass the userSettings object to the child component and run both functions, it runs with the wrong values which results in the data not saving properly.
Here is my code:
Parent component that has the toggle functions, handles the state, and set the userSettings object:
class SideMenu extends React.PureComponent {
constructor(props) {
super(props);
const userToggleSettings = {
cascadingPanels: this.props.userSettings.usesCascadingPanels,
includeAttachments: this.props.userSettings.alwaysIncludeAttachments,
analyticsOptIn: false,
};
this.state = {
userToggleSettings,
};
}
toggleIncludeAttachments = () => {
this.setState((prevState) => ({
userToggleSettings: {
...prevState.userToggleSettings,
includeAttachments: !prevState.userToggleSettings.includeAttachments,
},
}));
};
toggleCascadingPanels = () => {
this.setState((prevState) => ({
userToggleSettings: {
...prevState.userToggleSettings,
cascadingPanels: !prevState.userToggleSettings.cascadingPanels,
},
}));
};
includeAttachmentsClickHandler = () => {
this.toggleIncludeAttachments();
};
cascadingPanelsClickHandler = () => {
this.toggleCascadingPanels();
};
render() {
const darkThemeClass = this.props.isDarkTheme ? "dark-theme" : "";
const v2Class = this.state.machineCardV2Enabled ? "v2" : "";
const phAdjustmentStyle = this.getPersistentHeaderAdjustmentStyle();
const closeButton =
(this.state.machineListV2Enabled &&
this.props.view === sideMenuViews.USER_SETTINGS) ||
(!this.props.wrapper && this.props.view === sideMenuViews.SETTINGS);
return (
<div className="sideMenuFooter">
<SideMenuFooterContainer
userToggleSettings={this.state.userToggleSettings} //HERE IS USER_SETTINGS PASSED
/>
</div>
);
}
}
The child component that dispatches the data
SideMenuFooterContainer:
export function mapStateToProps(state) {
return {
translations: state.translations,
userSettings: state.appCustomizations.userSettings,
};
}
export function mapDispatchToProps(dispatch) {
return {
toggleCascadingPanels: (hasCascadingPanels) =>
dispatch(userSettingsDux.toggleCascadingPanels(hasCascadingPanels)),
toggleIncludeAttachments: (hasIncludeAttachments) =>
dispatch(userSettingsDux.toggleIncludeAttachments(hasIncludeAttachments)),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SideMenuFooter);
SideMenuFooterView (where it calls the dispatch):
const saveUserSettings = (props) => {
console.log("userSettings ==>\n");
console.log(props.userToggleSettings);
props.toggleIncludeAttachments(props.userToggleSettings.includeAttachments);
props.toggleCascadingPanels(props.userToggleSettings.cascadingPanels);
};
const cancelButtonClickHandler = (props) => {
if (props.viewTitle === props.translations.USER_SETTINGS) {
return () => props.closeSideMenu();
}
return () => props.viewBackButtonCallback();
};
const doneSaveButtonsClickHandler = (props) => {
return () => {
saveUserSettings(props);
props.closeSideMenu();
};
};
const SideMenuFooter = (props) => {
return (
<div className="side-menu-footer">
<div className="side-menu-footer-container">
<button
className="btn btn-secondary"
onClick={cancelButtonClickHandler(props)}
>
{props.translations.CANCEL}
</button>
<button
className="btn btn-primary"
onClick={doneSaveButtonsClickHandler(props)}
>
{props.translations.SAVE}
</button>
</div>
</div>
);
};
export default SideMenuFooter;
Dispatch functions:
export function toggleIncludeAttachments(hasIncludeAttachments) {
return async (dispatch, getState) => {
const { translations, appCustomizations } = getState();
const updatedUserSettings = {
...appCustomizations.userSettings,
alwaysIncludeAttachments: hasIncludeAttachments,
};
try {
await saveAppCustomizationByName(
CUSTOMIZATIONS.USER_SETTINGS,
updatedUserSettings
);
dispatch(setSettings(updatedUserSettings));
} catch (err) {
dispatch(
bannerDux.alertBanne({
description: "FAILED TO UPDATE USER DATA",
})
);
}
};
}
export function toggleCascadingPanels(hasCascadingPanels) {
return async (dispatch, getState) => {
const { translations, appCustomizations } = getState();
const updatedUserSettings = {
...appCustomizations.userSettings,
usesCascadingPanels: hasCascadingPanels,
};
try {
await saveAppCustomizationByName(
CUSTOMIZATIONS.USER_SETTINGS,
updatedUserSettings
);
dispatch(setSettings(updatedUserSettings));
} catch (err) {
dispatch(
bannerDux.alertBanner({
description: "FAILED TO UPDATE USER DATA",
})
);
}
};
}
Here is a demo:
When I set them both to false and console log the values, it looks like it is getting the correct values, but in the network call, it is getting different values on different calls
console.log output:
First network call to save data header values:
Second network call to save data header values:
NOTE: The dispatch functions work correctly, they where there before all the edits. I am changing the way it saves the data automatically to the save button using the same functions defined before.
Did I miss a step while approaching this, or did I mishandle the state somehow?
I created follow button component which cause a problem. It displays singularly on a tag page. On first load there is no error, but when I'm clicking other tag to display other tag's page then this error appears:
Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in FollowButton (at TagPage.tsx:79)
All answers I found on the internet says about adding isCancelled flag in useEffect hook, which I did, but it didn't help at all.
import React, { useEffect, useState, useContext } from "react";
import { Button } from "react-bootstrap";
import { FaRegEye } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import FollowInfo from "../models/dtos/read/FollowInfo";
import UsersService from "../services/UsersService";
import Viewer from "../models/Viewer";
import { ViewerContext } from "../ViewerContext";
interface Props {
for: "user" | "tag";
withId: number;
}
const FollowButton = (props: Props) => {
//todo too much rerenders, button actually blinks at start and show wrong state
const { t } = useTranslation();
const [followInfo, setFollowInfo] = useState<FollowInfo | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
console.log("follow rerender", followInfo);
let viewer: Viewer = useContext(ViewerContext);
useEffect(() => {
let isCancelled = false;
!isCancelled && setFollowInfo(null);
const fetchData = async () => {
if (props.for === "user") {
!isCancelled &&
setFollowInfo(
await UsersService.amIFollowingUser(
props.withId,
viewer.currentUser?.token
)
);
} else {
!isCancelled &&
setFollowInfo(
await UsersService.amIFollowingTag(
props.withId,
viewer.currentUser?.token
)
);
}
};
!isCancelled && fetchData();
return () => {
isCancelled = true;
};
}, [props, viewer.currentUser]);
const follow = (what: "tag" | "user", withId: number) => {
if (what === "user") {
followUser(withId);
} else {
followTag(withId);
}
setFollowInfo((state) => {
if (state != null) {
return { ...state, doesFollow: true };
} else {
return { receiveNotifications: false, doesFollow: true };
}
});
};
const unfollow = (what: "tag" | "user", withId: number) => {
if (what === "user") {
unfollowUser(withId);
} else {
unfollowTag(withId);
}
setFollowInfo((state) => {
if (state != null) {
return { ...state, doesFollow: false };
} else {
return { receiveNotifications: false, doesFollow: false };
}
});
};
const followUser = (userId: number) =>
makeRequest(() =>
UsersService.followUser(userId, viewer.currentUser?.token)
);
const unfollowUser = (userId: number) =>
makeRequest(() =>
UsersService.unfollowUser(userId, viewer.currentUser?.token)
);
const followTag = (tagId: number) =>
makeRequest(() => UsersService.followTag(tagId, viewer.currentUser?.token));
const unfollowTag = (tagId: number) =>
makeRequest(() =>
UsersService.unfollowTag(tagId, viewer.currentUser?.token)
);
const makeRequest = (call: () => Promise<any>) => {
setIsSubmitting(true);
call().then(() => setIsSubmitting(false));
};
return (
<>
{followInfo == null ? (
t("loading")
) : followInfo.doesFollow ? (
<Button
disabled={isSubmitting}
variant="light"
onClick={() => unfollow(props.for, props.withId)}
>
<FaRegEye />
{t("following")}
</Button>
) : (
<Button
disabled={isSubmitting}
onClick={() => follow(props.for, props.withId)}
>
<FaRegEye />
{t("follow")}
</Button>
)}
</>
);
};
export default FollowButton;
!isCancelled && setFollowInfo(await...) checks the flag and schedules setFollowInfo to execute when data is ready. The flag may change during await.
Try this:
if (!isCancelled) {
const data = await UsersService.amIFollowingUser(
props.withId,
viewer.currentUser?.token
);
!isCancelled && setFollowInfo(data);
}
Also check the documentation for AbortController. It will be better to use it inside UsersService.amIFollowing*
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);
})
};