i'm using the tab view react native library to build a user account screen. my request is simple, how can i update the tab view content after an api call that fetches the user data?
function UserStoreScreen({ navigation, route }) {
const layout = useWindowDimensions();
const [index, setIndex] = React.useState(0);
const [userStore, setUserStore] = React.useState({});
const [routes] = React.useState([
{ key: "first", title: "Dressing" },
{ key: "second", title: "À propos" },
]);
const user = route.params;
// renders undefined
const FirstRoute = () => (
<>
<View style={styles.userContainer}>
<ListItem
image={`${IMAGES_BASE_URL}${userStore.photo}`}
title={`${userStore.username}`}
subTitle={`${userStore.store.length} Articles`}
/>
</View>
</>
);
const SecondRoute = () => (
<>
<View style={{ flex: 1, backgroundColor: "#ff4081" }} />
</>
);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
const getUser = async () => {
await axiosApi
.post("/getUserProducts", { user_id: user.user_id })
.then((response) => {
// didn't work since set state is async
setUserStore(response.data);
})
.catch((err) => {
console.log(err);
});
};
// Get store products
useEffect(() => {
getUser();
}, []);
return (
<Screen style={styles.screen}>
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
}
is there a way to make the content of the tab view updated after i receive the data from the api call?
Yes, there is a way to forcefully re-mount a component. To do that, we can use key props like this:
return (
<Screen style={styles.screen}>
<TabView
key={JSON.stringify(userStore)}
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Screen>
);
How does key props work? Every time a component is re-rendering, it will check whether the key value is the same or not. If it's not the same, then force a component to re-render.
In this case we will always check if userStore value has changed or not.
Related
I am creating a ToDo app. This app has two screens: Todos and Done. I'm using BottomTabNavigator to switch between these screens. These two screens has list of todos. The todos component shows the undone todos and the Done component shows the done todos. There's a checkbox on the left and Trash icon on the right of every single todo. When a todo from Todos page is checked then it moves to the Done page. The issue is: after switching to the Done screen from Todos for the first time then after unchecking the todo there gives this warning:
Warning: 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 SingleTodo (at Done.tsx:74)
After this, the app is running perfectly. As I'm not sure which component is causing this error that'w why I'm sharing the minimal version of the code.
I have set up Bottom Tab navigator component like this:
import stuff..
...
const HomeTab = () => {
return (
<Tab.Navigator
screenOptions={({route}) => ({
headerShown: false,
tabBarIcon: ({focused, color, size}) => {
let iconName = '';
size = focused ? 25 : 20;
if (route.name === 'To-Do') {
iconName = 'clipboard-list';
} else if (route.name === 'Done') {
iconName = 'clipboard-check';
}
return <FontAwesome5Icon name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#0080ff',
tabBarInactiveTintColor: '#777777',
tabBarLabelStyle: {fontSize: 15, fontWeight: 'bold'},
})}>
<Tab.Screen name="To-Do" component={Todos} />
<Tab.Screen name="Done" component={Done} />
</Tab.Navigator>
);
};
export default HomeTab;
As you can see, there are 2 components here. One is Todos. The code for this component is as follows:
import stuff...
...
const Todos = ({navigation}) => {
const dispatch = useAppDispatch();
const {todos}: {todos: TodoInterface[]} = useAppSelector(
state => state.todoReducer,
);
useEffect(() => {
loadTodos();
}, []);
const loadTodos = () => {
AsyncStorage.getItem('todos').then(todos => {
const parsedTodos: TodoInterface[] = JSON.parse(todos || '{}');
dispatch(setAllTodo(parsedTodos));
});
};
return (
<HideKeyboard>
<View style={styles.body}>
<FlatList
data={todos.filter(todo => todo.done !== true)}
renderItem={({item, index}) => {
const firstChild = index == 0 ? {marginTop: 5} : {};
return (
<TouchableOpacity
style={[styles.todoWrp, firstChild]}
onPress={() => todoPressHandler(item.todoId)}>
<SingleTodo // ***The code for this one is given below***
title={item.title}
subtitle={item.subTitle}
done={item?.done}
todoId={item.todoId}
/>
</TouchableOpacity>
);
}}
keyExtractor={(item, i) => item.todoId}
/>
<TouchableOpacity style={styles.addBtn} onPress={addBtnHandler}>
<FontAwesome5 name="plus" color="#fff" size={25} />
</TouchableOpacity>
</View>
</HideKeyboard>
);
}
The code for SingleTodo is as follows:
const SingleTodo = ({title, subtitle, done: doneProp, todoId}: Props) => {
const [done, setDone] = useState(doneProp);
const dispatch = useAppDispatch();
const {todos}: TodosType = useAppSelector(state => state.todoReducer);
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
return (
<View style={styles.body}>
<CheckBox
value={done}
onValueChange={val => checkBoxHandler(val)}
style={styles.checkbox}
/>
<View>
<Text style={[styles.title, GlobalStyle.IndieFont]}>{title}</Text>
<Text style={[styles.subtitle, GlobalStyle.IndieFont]}>{subtitle}</Text>
</View>
<View style={styles.trashWrp}>
<TouchableOpacity onPress={deleteHandler}>
<FontAwesome5Icon
style={styles.trashIcon}
name="trash"
color="#e74c3c"
size={20}
/>
</TouchableOpacity>
</View>
</View>
);
};
export default SingleTodo;
The code for Done component is similar to Todos component. The only changes is on the data property of the component
<FlatList
data={todos.filter(todo => todo.done === true)}
...
other props...
...
/>
It's happening every time you use this, it is just shown once to not spam the console.
const checkBoxHandler = (val: boolean) => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList[index].done = val;
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
setDone(val);
});
};
const deleteHandler = () => {
const todoList: TodoInterface[] = [...todos];
const index = todos.findIndex(todo => todo.todoId === todoId);
todoList.splice(index, 1);
AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
dispatch(setAllTodo(todoList));
});
};
Basically, you call the function, and the todo is unmounted from the state, but the function is not completed yet and you get that warning.
The solution is to lift everything related to the deleteHandler and checkBoxHandler from your children (Todo) to your parent (Todos), and pass it to Todo as props. Since parent is always mounted, deleting the todo will not unmount the parent and therefore, delete function will not be interrupted.
I am trying to save a value to async storage and then navigate to the right page depending on what the value outcome is from the Async storage. I can store data in AsyncStorage but my states does not update, I have to reload the app in order for the state to update. here is my code:
Here I have a Welcome/Obnoarding screen. I want this screen to only show to the new app users. So when a user presses the continue button I want to save a value to the Async storage so that the next time they log in they don't have to see the onboarding page again. Here is my Onboarding page:
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
In my AppNavigator I want to decide which navigation the user should see. But when I press the continue page my app does not navigate to my TabsNavigator. It stays on my Onboarding page but if I refresh the app then the app navigates to my Tabs navigator. here is the code where I determine where the user should be depending if they are a new user or a "old" user:
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
Setting a value in the async storage will not trigger a rerender of your AppNavigator. Thus, if the user presses the continue button, then nothing will happen visually, since the state of AppNavigator has not changed. If you refresh the app, the flag, which you have set previously using the setItem function, will be reloaded in AppNavigator on initial rendering. This is the reason why it works after refreshing the application.
For this kind of problem, I would suggest that you use a Context for triggering a state change in AppNavigator.
Here is a minimal example on how this would work. I have added comments in the code to guide you.
For the sake of simplicity, we will make the following assumption:
We have two screens in a Stack, one is the WelcomeScreen, the other one is called HomeScreen.
Notice that we use conditional rendering for the screens depending on our application context. You can add whatever screens you want, even whole navigators (this would be necessary if your navigators are nested, but the pattern stays the same).
App
export const AppContext = React.createContext()
const App = () => {
// it is important that the initial state is undefined, since
// we need to wait for the async storage to return its value
// before rendering anything
const [hasViewedOnboarding, setHasViewedOnboarding] = React.useState()
const appContextValue = useMemo(
() => ({
hasViewedOnboarding,
setHasViewedOnboarding,
}),
[hasViewedOnboarding]
)
// retrieve the onboarding flag from the async storage in a useEffect
React.useEffect(() => {
const init = async () => {
const value = await AsyncStorage.getItem('#viewedOnboarding')
setHasViewedOnboarding(value != null ? JSON.parse(value) : false)
}
init()
}, [])
// as long as the flag has not been loaded, return null
if (hasViewedOnboarding === undefined) {
return null
}
// wrap everything in AppContext.Provider an pass the context as a value
return (
<AppContext.Provider value={appContextValue}>
<NavigationContainer>
<Stack.Navigator>
{!hasViewedOnboarding ? (
<Stack.Screen name="Welcome" component={WelcomeScreen} />
) : (
<Stack.Screen
name="Home"
component={HomeScreen}
/>
)}}
</Stack.Navigator>
</NavigationContainer>
</AppContext.Provider>
)
}
Now, in your WelcomeScreen you need to access the context and set the state after the async value has been stored.
const WelcomeScreen: FC<IWelcomeScreen> = ({ navigation }) => {
// access the context
const { setHasViewedOnboarding } = useContext(AppContext)
const { width, height } = Dimensions.get("window");
const btnText = "Contiunue";
const title = "Book";
const subTitle = "Fab";
let [fontsLoaded] = useFonts({
PinyonScript_400Regular,
});
const continueBtn = async () => {
try {
await AsyncStorage.setItem('#viewedOnboarding', 'true');
setHasViewedOnboarding(true)
} catch (error) {
console.log('Error #setItem: ', error);
};
};
if (!fontsLoaded) {
return <Text>...Loading</Text>;
} else {
return (
<View style={containerStyle(height, width).container}>
<ImageBackground
resizeMode={"cover"}
style={styles.image}
source={require("../assets/model.jpg")}
>
<LinearGradient
colors={["#00000000", "#000000"]}
style={styles.gradient}
>
<View style={styles.container}>
<View style={styles.logoTextContainer}>
<Text style={styles.logoText}>{title}</Text>
<Text style={styles.logoText}>{subTitle}</Text>
</View>
<ContinueBtn label={btnText} callback={continueBtn} />
</View>
</LinearGradient>
</ImageBackground>
</View>
);
}
};
I have a useEffect() which should be called once but continuously it is calling the getStories method.
const [pageNo, setPageNo] = useState(1);
const [recordsPerPage, setrecordsPerPage] = useState(5);
const { jwt } = useSelector((state: StateParams) => state.account);
useEffect(() => {
if (jwt) {
dispatch(
getStories({ token: jwt, pageNo: pageNo, recordsPerPage: recordsPerPage })
);
}
}, []);
UPDATED: I feel flatlist is causing
<FlatList
style={{
marginTop: 14,
alignSelf: "stretch"
}}
data={Items}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
const renderItem = useCallback(({ item } : {id: string}) => {
console.log({item})
const Section = Components[item.id]
return (<Section {...props}/>)
}, [Items])
How do I invalidate data after refreshing the page? It doesn't seem to invalidate while it is supposed to. It still displays the old data even though something changed on the server-side.
I have this same problem when I use useMutation when posting data to the backend, the UI doesn't update even after using the QueryClient.
Below is my code:
const IncomeManager: React.FC<any> = (props) => {
const queryClient = new QueryClient();
const {isLoading, isError, isFetching, data}: QueryObserverResult = useQuery('typeIncomes', () => typesApi.getAllTypes());
const [refresh, setRefresh] = useState<boolean>(false);
const [isModalVisible, setIsModalVisible] = useState<boolean>(false);
//#ts-ignore
const handleClose = (): void => {
setIsModalVisible(false);
}
const refreshContent = async () => {
await queryClient.invalidateQueries('typeIncomes');
console.log("Content has been refreshed!!!");
}
return (
<View style={style.container}>
<View>
<AppText style={style.title}>{data ? data.length : 0} income types available</AppText>
</View>
<FixedButton
title={"plus"}
onPress={() => props.navigation.navigate(navConstants.ADDTYPE, {type: "incomes"})}
/>
{
isLoading || isFetching ? <PageActivityIndicator visible={isLoading || isFetching}/> :
<FlatList style={{width: "100%"}}
data={data}
renderItem={
({item}) => <CategoryItem
id={item.type_id}
title={item.title}
subTitle={item.description}
onLongPress={() => console.log("Very long press!")}
onPress={() => props.navigation.navigate(navConstants.EDITTYPE,
{
item: {
id: item.type_id,
title: item.title,
description: item.description
}
})
}
/>
}
keyExtractor={item => item.type_id}
refreshing={refresh}
onRefresh={async () => refreshContent()}
/>
}
</View>
);
}
export default IncomeManager;
const style = StyleSheet.create({
container: {
flex: 1,
width: "100%",
backgroundColor: constants.COLORS.secondary,
alignItems: "center"
},
title: {
color: constant.COLORS.lightGray,
paddingVertical: 10,
fontSize: 17,
marginBottom: 0
},
});
You are creating a new QueryClient every time your component renders by doing:
const queryClient = new QueryClient()
The queryClient holds your cache, which holds your data. There should be only one (like a redux store) - the one you create initially and then pass to the QueryClientProvider. To retrieve this Singleton instance, you can do:
const queryClient = useQueryClient()
it will give you the instance via React context. Invalidation on that queryClient should work. This is also how everything in the docs and all the example are set up.
I have a fairly basic FlatList component implemented using hooks. The list simply loads data from a random user API and lazy loads additional data via infinite scroll. The only visual issue I'm experiencing is that when I merge the new data with the current, the new data being appended flickers very briefly before fully rendering. Not sure what could be causing this.
Expo Snack
const useRestApi = (url) => {
const [ data, setPeople ] = useState([]);
const [ page, setPage ] = useState(1);
const [ results, setResults ] = useState(20);
const [ loading, setLoading ] = useState(false);
useEffect(() => {
const fetchPeople = async () => {
setLoading(true);
const response = await fetch(`${url}&page=${page}&results=${results}`);
const json = await response.json();
if(page !== 1)
setPeople([...data, ...json.results]);
else
setPeople(json.results);
setLoading(false);
}
fetchPeople();
}, [page]);
return [{data, loading, page}, setPage, setResults];
}
const App: () => React$Node = () => {
const [{ data: people, loading, page }, setPage, setResults] = useRestApi(`https://randomuser.me/api?&seed=ieee`);
return (
<>
<StatusBar barStyle="dark-content" />
<SafeAreaView>
<FlatList
data={people}
onEndReachedThreshold={0.2}
keyExtractor={(item, index) => index.toString()}
renderItem={({item, index}) => (
<View key={index} style={styles.listItem}>
<Text style={styles.listItemHeader}>{item.name.first} {item.name.last}</Text>
<Text style={styles.listItemSubHeader}>{item.location.country}</Text>
<Text style={styles.listItemBody}>{item.location.street.number} {item.location.street.name}</Text>
<Text style={styles.listItemBody}>{item.location.city} {item.location.state} {item.location.postcode}</Text>
</View>
)}
refreshing={loading}
onRefresh={() => {setResults(20); setPage(1);}}
onEndReached={() => {setResults(5); setPage(page + 1);}}
ItemSeparatorComponent={() => ItemSeparatorComponent}
ListFooterComponent={() => loading ? ListFooterComponent : null}
/>
</SafeAreaView>
</>
);
};