How to make dynamic tab view screen in react native - reactjs

I am trying to add tabs in my react native app. Here on tab i want to show all the data coming from an api. This gives a array of string. And when user click on any tab it should show respective data. Here is an example image.
Here below header I want to display the array of string coming from the ap.
Below the search field I want to display the data which is coming from different api.
I am using a package https://www.npmjs.com/package/react-native-tab-view . I am not sure how to achieve this with this.
Here is the code I have
import { TabView, SceneMap } from "react-native-tab-view";
import { connect } from "react-redux";
import { getAllState } from "../../actions/hubActions";
interface CommunityMemberProps {
getStates: () => void;
allStates: [];
}
const styles = StyleSheet.create({
scene: {
flex: 1,
},
});
const FirstRoute = () => (
<View style={[styles.scene, { backgroundColor: "#ff4081" }]} />
);
const SecondRoute = () => (
<View style={[styles.scene, { backgroundColor: "#673ab7" }]} />
);
const initialLayout = { width: Dimensions.get("window").width };
const CommunityMember = ({ getStates, allStates }: CommunityMemberProps) => {
useEffect(() => {
getStates();
}, []);
const [searchText, setSearchText] = useState<string>("");
const handleChangeText = (text: string) => {
setSearchText(text);
};
console.log("allStates", allStates); <-- this gives data ["India", "newDelhi"]
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{ key: "First", title: "First" },
{ key: "Second", title: "Second" },
]);
const renderScene = SceneMap({
first: FirstRoute,
second: SecondRoute,
});
return (
<TabView
navigationState={{ index, routes }}
renderScene={renderScene}
onIndexChange={setIndex}
initialLayout={initialLayout}
/>
);
};
function mapStateToProps(state: any) {
return {
allStates: state.hub.allStates,
};
}
const mapDispatchToProps = (dispatch: any) => ({
getStates: () => dispatch(getAllState()),
});
export default connect(mapStateToProps, mapDispatchToProps)(CommunityMember);

Since you are using redux this can be easily done. Whenever you select a tab update the redux state with the tab selected. Keep the component same for all tabs and retrieve the tab from redux state using mapStateToProps and fetch data dynamically and useEffect or componentDidMount() hook.

Related

Passing Array into FlatList React Native (Not Rendering)

So I have this array, as shown below:
const categoryData=[
{
id: 1,
name: "cat1",
icon: icons.one
},
{
id: 2,
name: "cat2",
icon: icons.two
},
]
I'm trying to store it in a useState const called categories, and then display categories in the data parameter within a FlatList:
const [categories, setCategories] = React.useState(categoryData)
<FlatList
data={categories}
keyExtractor={item => `${item.id}`}
renderItem={renderItem}
numColumns={4}
contentContainerStyle={{ padding: SIZES.padding }}
/>
However, nothing renders when within the FlatList (important to note, the FlatList loads when a Modal is triggered, and the FlatList does render the items properly when data={categoryData} instead of just categories).
Thanks for the assistance.
EDIT: Here is an Expo link to depict the issue (https://snack.expo.dev/HsffYfsrc)
In your code example you've used/referenced categoryData prior to it having been declared.
const App = () => {
const [categories, setCategories] = React.useState(categoryData); // <-- used, undefined
const [modalVisible, setModalVisible] = React.useState(false);
const categoryData = [ // <-- declared here
{
id: 1,
name: 'cat1',
icon: 'test'
},
{
id: 2,
name: 'cat2',
icon: 'test'
},
];
function renderList() {
...
}
return <SafeAreaView>{renderList()}</SafeAreaView>;
}
I don't see any internal dependencies (to the component) to require that categoryData be declared within the component. I suggest declaring it outside the component so it's declared prior to the component and in scope.
const categoryData = [ // <-- declared
{
id: 1,
name: 'cat1',
icon: 'test',
},
{
id: 2,
name: 'cat2',
icon: 'test',
},
];
const App = () => {
const [categories, setCategories] = React.useState(categoryData); // <-- defined
const [modalVisible, setModalVisible] = React.useState(false);
function renderList() {
...
}
return <SafeAreaView>{renderList()}</SafeAreaView>;
};
Expo Snack
If categoryData is not known at compile time and is actually fetched later then provide valid initial state and load/update the category state in an useEffect hook.
Example:
const App = () => {
const [categories, setCategories] = React.useState([]); // <-- valid initial state
const [modalVisible, setModalVisible] = React.useState(false);
useEffect(() => {
... fetch/loading logic
setModalVisible(data); // <-- update state with fetched/loaded data
}, []);
function renderList() {
...
}
return <SafeAreaView>{renderList()}</SafeAreaView>;
};
If you say it works when Modal is triggered, then the issue might be that your data is not ready on load.
I suggest you try:
{categories && <FlatList
data={categories}
keyExtractor={item => `${item.id}`}
renderItem={renderItem}
numColumns={4}
contentContainerStyle={{ padding: SIZES.padding }}/>}

How to make data persist on refresh React JS?

I have a code where I mount a table with some firebase data but for some reason the values disappear and I been struggling for the next 2 weeks trying to solve this issue I haven't found a solution to this and I have asked twice already and I have try everything so far but it keeps disappearing.
Important Update
I just want to clarify the following apparently I was wrong the issue wasn't because it was a nested collection as someone mentioned in another question. The issue is because my "user" is getting lost in the process when I refresh.
I bring the user from the login to the app like this:
<Estudiantes user={user} />
and then I receive it as a props
function ListadoPedidos({user})
but is getting lost and because is getting lost when I try to use my firebase as:
estudiantesRef = db.collection("usuarios").doc(user.uid).collection("estudiantes")
since the user is "lost" then the uid will be null. Since is null it will never reach the collection and the docs.
I have a simple solution for you. Simply raise the parsing of localStorage up one level, passing the preloadedState into your component as a prop, and then using that to initialize your state variable.
const ListadoEstudiantes = (props) => {
const estData = JSON.parse(window.localStorage.getItem('estudiantes'));
return <Listado preloadedState={estData} {...props} />;
};
Then initialize state with the prop
const initialState = props.preloadedState || [];
const [estudiantesData, setEstudiantesData] = useState(initialState);
And finally, update the useEffect hook to persist state any time it changes.
useEffect(() => {
window.localStorage.setItem('estudiantes', JSON.stringify(estudiantes));
}, [estudiantes]);
Full Code
import React, { useState, useEffect } from 'react';
import { db } from './firebase';
import { useHistory } from 'react-router-dom';
import './ListadoEstudiantes.css';
import {
DataGrid,
GridToolbarContainer,
GridToolbarFilterButton,
GridToolbarDensitySelector,
} from '#mui/x-data-grid';
import { Button, Container } from '#material-ui/core';
import { IconButton } from '#mui/material';
import PersonAddIcon from '#mui/icons-material/PersonAddSharp';
import ShoppingCartSharpIcon from '#mui/icons-material/ShoppingCartSharp';
import DeleteOutlinedIcon from '#mui/icons-material/DeleteOutlined';
import { Box } from '#mui/system';
const ListadoEstudiantes = (props) => {
const estData = JSON.parse(window.localStorage.getItem('estudiantes'));
return <Listado preloadedState={estData} {...props} />;
};
const Listado = ({ user, preloadedState }) => {
const history = useHistory('');
const crearEstudiante = () => {
history.push('/Crear_Estudiante');
};
const initialState = preloadedState || [];
const [estudiantesData, setEstudiantesData] = useState(initialState);
const parseData = {
pathname: '/Crear_Pedidos',
data: estudiantesData,
};
const realizarPedidos = () => {
if (estudiantesData == 0) {
window.alert('Seleccione al menos un estudiante');
} else {
history.push(parseData);
}
};
function CustomToolbar() {
return (
<GridToolbarContainer>
<GridToolbarFilterButton />
<GridToolbarDensitySelector />
</GridToolbarContainer>
);
}
const [estudiantes, setEstudiantes] = useState([]);
const [selectionModel, setSelectionModel] = useState([]);
const columns = [
{ field: 'id', headerName: 'ID', width: 100 },
{ field: 'nombre', headerName: 'Nombre', width: 200 },
{ field: 'colegio', headerName: 'Colegio', width: 250 },
{ field: 'grado', headerName: 'Grado', width: 150 },
{
field: 'delete',
width: 75,
sortable: false,
disableColumnMenu: true,
renderHeader: () => {
return (
<IconButton
onClick={() => {
const selectedIDs = new Set(selectionModel);
estudiantes
.filter((x) => selectedIDs.has(x.id))
.map((x) => {
db.collection('usuarios')
.doc(user.uid)
.collection('estudiantes')
.doc(x.uid)
.delete();
});
}}
>
<DeleteOutlinedIcon />
</IconButton>
);
},
},
];
const deleteProduct = (estudiante) => {
if (window.confirm('Quiere borrar este estudiante ?')) {
db.collection('usuarios').doc(user.uid).collection('estudiantes').doc(estudiante).delete();
}
};
useEffect(() => {}, [estudiantesData]);
const estudiantesRef = db.collection('usuarios').doc(user.uid).collection('estudiantes');
useEffect(() => {
estudiantesRef.onSnapshot((snapshot) => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setEstudiantes(tempData);
console.log(estudiantes);
});
}, []);
useEffect(() => {
window.localStorage.setItem('estudiantes', JSON.stringify(estudiantes));
}, [estudiantes]);
return (
<Container fixed>
<Box mb={5} pt={2} sx={{ textAlign: 'center' }}>
<Button
startIcon={<PersonAddIcon />}
variant="contained"
color="primary"
size="medium"
onClick={crearEstudiante}
>
Crear Estudiantes
</Button>
<Box pl={25} pt={2} mb={2} sx={{ height: '390px', width: '850px', textAlign: 'center' }}>
<DataGrid
rows={estudiantes}
columns={columns}
pageSize={5}
rowsPerPageOptions={[5]}
components={{
Toolbar: CustomToolbar,
}}
checkboxSelection
//Store Data from the row in another variable
onSelectionModelChange={(id) => {
setSelectionModel(id);
const selectedIDs = new Set(id);
const selectedRowData = estudiantes.filter((row) => selectedIDs.has(row.id));
setEstudiantesData(selectedRowData);
}}
{...estudiantes}
/>
</Box>
<Button
startIcon={<ShoppingCartSharpIcon />}
variant="contained"
color="primary"
size="medium"
onClick={realizarPedidos}
>
Crear pedido
</Button>
</Box>
</Container>
);
};
I suspect that it's because this useEffect does not have a dependency array and is bring run on every render.
useEffect (() => {
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes))
})
Try adding a dependency array as follows:
useEffect (() => {
if (estudiantes && estudiantes.length>0)
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes))
},[estudiantes])
This will still set the localStorage to [] when it runs on the first render. But when the data is fetched and estudiantes is set, the localStorage value will be updated. So I've added a check to check if it's not the empty array.
Change the dependency array of this useEffect to []:
estudiantesRef.onSnapshot(snapshot => {
const tempData = [];
snapshot.forEach((doc) => {
const data = doc.data();
tempData.push(data);
});
setEstudiantes(tempData);
console.log(estudiantes)
})
}, []);
The data flow in your code is somewhat contradictory, so I modify your code, and it works fine.
You can also try delete or add button, it will modify firebase collection, then update local data.
You can click refresh button in codesandbox previewer (not browser) to observe the status of data update.
Here is the code fargment :
// Set value of `localStorage` to component state if it exist.
useEffect(() => {
const localStorageEstData = window.localStorage.getItem("estudiantes");
localStorageEstData && setEstudiantes(JSON.parse(localStorageEstData));
}, []);
// Sync remote data from firebase to local component data state.
useEffect(() => {
// Subscribe onSnapshot
const unSubscribe = onSnapshot(
collection(db, "usuarios", user.id, "estudiantes"),
(snapshot) => {
const remoteDataSource = snapshot.docs.map((doc) => ({
id: doc.id,
...doc.data()
}));
console.info(remoteDataSource);
setEstudiantes(remoteDataSource);
}
);
return () => {
//unSubscribe when component unmount.
unSubscribe();
};
}, [user.id]);
// when `estudiantes` state update, `localStorage` will update too.
useEffect(() => {
window.localStorage.setItem("estudiantes", JSON.stringify(estudiantes));
}, [estudiantes]);
Here is the full code sample :
Hope to help you :)

Best way to use useMemo/React.memo for an array of objects to prevent rerender after edit one of it?

I'm struggling with s performance issue with my React application.
For example, I have a list of cards which you can add a like like facebook.
Everything, all list is rerendering once one of the child is updated so here I'm trying to make use of useMemo or React.memo.
I thought I could use React.memo for card component but didn't work out.
Not sure if I'm missing some important part..
Parent.js
const Parent = () => {
const postLike= usePostLike()
const listData = useRecoilValue(getCardList)
// listData looks like this ->
//[{ id:1, name: "Rose", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc },
// { id:2, name: "Helena", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc },
// { id: 3, name: "Gutsy", avararImg: "url", image: "url", bodyText: "text", liked: false, likedNum: 1, ...etc }]
const memoizedListData = useMemo(() => {
return listData.map(data => {
return data
})
}, [listData])
return (
<Wrapper>
{memoizedListData.map(data => {
return (
<Child
key={data.id}
data={data}
postLike={postLike}
/>
)
})}
</Wrapper>
)
}
export default Parent
usePostLike.js
export const usePressLike = () => {
const toggleIsSending = useSetRecoilState(isSendingLike)
const setList = useSetRecoilState(getCardList)
const asyncCurrentData = useRecoilCallback(
({ snapshot }) =>
async () => {
const data = await snapshot.getPromise(getCardList)
return data
}
)
const pressLike = useCallback(
async (id) => {
toggleIsSending(true)
const currentList = await asyncCurrentData()
...some api calls but ignore now
const response = await fetch(url, {
...blabla
})
if (currentList.length !== 0) {
const newList = currentList.map(list => {
if (id === list.id) {
return {
...list,
liked: true,
likedNum: list.likedNum + 1,
}
}
return list
})
setList(newList)
}
toggleIsSending(false)
}
},
[setList, sendYell]
)
return pressLike
}
Child.js
const Child = ({
postLike,
data
}) => {
const { id, name, avatarImg, image, bodyText, likedNum, liked } = data;
const onClickPostLike = useCallback(() => {
postLike(id)
})
return (
<Card>
// This is Material UI component
<CardHeader
avatar={<StyledAvatar src={avatarImg} />}
title={name}
subheader={<SomeImage />}
/>
<Image drc={image} />
<div>{bodyText}</div>
<LikeButton
onClickPostLike={onClickPostLike}
liked={liked}
likedNum={likedNum}
/>
</Card>
)
}
export default Child
LikeButton.js
const LikeButton = ({ onClickPostLike, like, likedNum }) => {
const isSending = useRecoilValue(isSendingLike)
return (
<Button
onClick={() => {
if (isSending) return;
onClickPostLike()
}}
>
{liked ? <ColoredLikeIcon /> : <UnColoredLikeIcon />}
<span> {likedNum} </span>
</Button>
)
}
export default LikeButton
The main question here is, what is the best way to use Memos when one of the lists is updated. Memorizing the whole list or each child list in the Parent component, or use React.memo in a child component...(But imagine other things could change too if a user edits them. e.g.text, image...)
Always I see the Parent component is highlighted with React dev tool.
use React.memo in a child component
You can do this and provide a custom comparator function:
const Child = React.memo(
({
postLike,
data
}) => {...},
(prevProps, nextProps) => prevProps.data.liked === nextProps.data.liked
);
Your current use of useMemo doesn't do anything. You can use useMemo as a performance optimization when your component has other state updates and you need to compute an expensive value. Say you have a collapsible panel that displays a list:
const [expand, setExpand] = useState(true);
const serverData = useData();
const transformedData = useMemo(() =>
transformData(serverData),
[serverData]);
return (...);
useMemo makes it so you don't re-transform the serverData every time the user expands/collapses the panel.
Note, this is sort of a contrived example if you are doing the fetching yourself in an effect, but it does apply for some common libraries like React Apollo.

Why is the component fully re-rendering when updating a single state through context?

I have created a page which has two columns:
In one column the idea is to display a list of items
On the other column, I should show some info related to the selected item
The code I have so far is:
import { INavLink, INavLinkGroup, INavStyles, Nav } from "#fluentui/react";
import React, { createContext, useContext, useState } from "react";
interface HistoryTtem {
id: string;
}
interface AppState {
selectedItem: string | undefined;
updateSelectedItem: (value: string | undefined) => void;
items: Array<HistoryTtem>;
}
const AppContext = createContext<AppState>({
selectedItem: undefined,
updateSelectedItem: (value: string | undefined) => {},
items: []
});
const App = () => {
const Column1 = () => {
const rootState: AppState = useContext(AppContext);
const getNavLinks: Array<INavLink> = rootState.items.map((item) => ({
name: item.id,
key: item.id,
url: ""
}));
const groups: Array<INavLinkGroup> = [
{
links: getNavLinks
}
];
const navStyles: Partial<INavStyles> = {
root: {
boxSizing: "border-box",
border: `1px solid #eee`,
overflowY: "auto"
}
};
const onItemClick = (
e?: React.MouseEvent<HTMLElement>,
item?: INavLink
) => {
if (item && item.key) {
rootState.updateSelectedItem(item.key);
}
};
return (
<Nav
onLinkClick={onItemClick}
selectedKey={rootState.selectedItem}
ariaLabel="List of previously searched transactions"
styles={navStyles}
groups={groups}
/>
);
};
const Column2 = () => {
return <div>aaa</div>;
};
const [historyItems, setHistoryItems] = useState<Array<HistoryTtem>>([
{
id: "349458457"
},
{
id: "438487484"
},
{
id: "348348845"
},
{
id: "093834845"
}
]);
const [selectedItem, setSelectedItem] = useState<string>();
const updateSelectedItem = (value: string | undefined) => {
setSelectedItem(value);
};
const state: AppState = {
selectedItem: selectedItem,
updateSelectedItem: updateSelectedItem,
items: historyItems
};
return (
<AppContext.Provider value={state}>
<div>
<Column1 />
<Column2 />
</div>
</AppContext.Provider>
);
};
export default App;
As you can see, I have a root state which will serve to drive the update of the second column triggered from inside the first one. But it is not working. When I click on an item, the whole component in the first column is re-rendering, while it should only change the selected item.
Please find here the CodeSandbox.
You shouldn't nest component functions.
The identity of Column1 changes for every render of App since it's an inner function, and that makes React think it needs to reconcile everything.
Move Column1 and Column2 up to the module level.
What makes react rerender is two things:
Change in State
Change in Props
You have an App Component which is the root of your components and it has a selectedItem state which is changing when an item is clicked so you have a new state and the new state will cause rerender

How to use Infinite Scroll with FlatList and React Hook?

I'm refactoring to React Hooks but I can't get Infinite Scroll with FlatList working.
const [page, setPage] = useState(1);
This is my useEffect Hook:
useEffect(() => {
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
setProducts([...products, ...results.data]);
setIsLoading(false);
};
loadProducts();
}, [page]);
Offset is ${page}, limit is &perPage=5 (hardcoded to 5)
Flatlist:
<FlatList
data={products}
keyExtractor={(item) => item.id}
renderItem={renderGridItem}
onEndReached={loadMore}
onEndThreshold={0.3}
/>;
LoadMore:
const loadMore = () => {
setPage(page + 1);
};
In theory, this should work, shouldn't it?
Description
I was struggling a lot with this myself. Here's an example using a SectionList (basically the same as a Flatlist)
The header numbers indicates the request number send to the API. You can check that the request are in the correct order and that there are no duplicates, by clicking the "Check Numbers" button.
In this example we use reqres.in to simulate a fetch to some data.
The example also implements pull-to-refresh. Again, you can check that the length of the array is as expected after a pull-to-refresh by clicking the "Check length" button.
Expo snack
A snack of the example can be found here: https://snack.expo.io/BydyF9yRH
Make sure to change platform to iOS or Android in the snack (Web will not work)
Code
import * as React from 'react';
import { ActivityIndicator } from 'react-native'
var _ = require('lodash')
import {
StyleSheet,
Text,
View,
SafeAreaView,
SectionList,
Button,
RefreshControl
} from 'react-native';
function Item(item) {
return (
<View style={styles.item}>
<Text style={styles.title}>{item.title.first_name}</Text>
</View>
);
}
export default function testSectionList({ navigation }) {
const [data, setData] = React.useState()
const [loading, setLoading] = React.useState(true)
const [refreshing, setRefreshing] = React.useState(false);
const [showRefreshingIndicator, setShowRefreshingIndicator] = React.useState(false);
const dataIndex = React.useRef(0);
const totalHits = React.useRef(42); // In real example: Update this with first result from api
const fetchData = async (reset: boolean) => {
if (reset === true) dataIndex.current = 0;
// Make sure to return if no more data from API
if (dataIndex.current !== 0 && dataIndex.current >= totalHits.current) return []
// For example usage, select a random page
const fakepage = Math.round(Math.random()) * 2
const resultObject = await fetch(`https://reqres.in/api/users?page=${fakepage}`);
const result = await resultObject.json()
dataIndex.current++;
return {
title: `${dataIndex.current-1}`,
data: await result.data
}
}
const count = () => {
alert(data.length)
}
const checkPageNumbers = () => {
const numbers = data.map(item => parseInt(item.title))
const incremental = [...Array(data.length).keys()]
alert(_.isEqual(numbers, incremental))
}
const getInitialData = async () => {
const list = await fetchData(false)
if(!list) return
setData([list])
setLoading(false)
}
React.useEffect(() => {
getInitialData()
}, [])
const onEndReached = async () => {
const newItems = await fetchData(false)
if(!newItems.data.length) return
setData([...data, newItems])
}
const onRefresh = React.useCallback(async () => {
setShowRefreshingIndicator(true);
const newItems = await fetchData(true)
setData([newItems])
setShowRefreshingIndicator(false)
}, [refreshing]);
if (loading) return <Text>Loading...</Text>
return (
<SafeAreaView style={styles.container}>
<Button title={"Check numbers"} onPress={() => checkPageNumbers()} />
<Button title={"Check length"} onPress={() => count()} />
<SectionList
sections={data}
refreshing={refreshing}
refreshControl={
<RefreshControl refreshing={showRefreshingIndicator} onRefresh={onRefresh} />
}
onEndReached={() => {
if(refreshing) return;
setRefreshing(true)
onEndReached().then(() => {
setRefreshing(false)
})
}}
onEndReachedThreshold={1}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => <Item title={item} />}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.header}>{title}</Text>
)}
ListFooterComponent={<ActivityIndicator size={"large"} />}
stickySectionHeadersEnabled={false}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 40,
marginHorizontal: 16,
},
item: {
backgroundColor: '#f9c2ff',
padding: 2,
marginVertical: 2,
},
header: {
fontSize: 16,
},
title: {
fontSize: 12,
},
});
Try to use useCallback instead of useEffect on this case. Also, I've shown you how you can prevent spreading null result to setState.
const loadProducts = async () => {
setIsLoading(true);
let response = await fetch(`${api}&page=${page}&perPage=5`);
let results = await response.json();
if (result.data) {
setProducts([...products, ...results.data]);
}
setIsLoading(false);
};
useEffect(() => {
loadProducts();
}, [])
const onLoadMore = useCallback(() => {
loadProducts();
}
for more information about useCallback, please read this. https://reactjs.org/docs/hooks-reference.html#usecallback

Resources