My FlatList renderItem was re-rendering every item when one of them was changed.
After doing some debugging i deepcloned the state variable which holds the items (+ React.memo), it's working fine now but not sure if it's the optimal solution.
Snack: https://snack.expo.io/-419PhiUl
App.js
import * as React from 'react';
import { View, StyleSheet, FlatList } from 'react-native';
import Constants from 'expo-constants';
import _ from 'lodash';
import Item from './components/Item';
const keyExtractor = item => item.id.toString();
export default function App() {
const [data, setData] = React.useState([
{id: 1, title: 'Post 1', liked: false, user: {name: 'A'}},
{id: 2, title: 'Post 2', liked: false, user: {name: 'B'}},
{id: 3, title: 'Post 3', liked: false, user: {name: 'C'}},
]);
/**
* Like / Unlike the item.
*/
const like = React.useCallback((id) => {
setData(state => {
let clonedState = [...state];
let index = clonedState.findIndex(item => item.id === id);
clonedState[index].liked = ! clonedState[index].liked;
return clonedState;
});
}, []);
/**
* Render items.
*/
const renderItem = React.useCallback(({item}) => (
<Item item={item} onLike={like} />
), []);
const deepClonedData = React.useMemo(() => _.cloneDeep(data), [data]);
return (
<View style={styles.container}>
<FlatList
data={deepClonedData}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
}
});
Item.js
import React from 'react';
import {
Text, TouchableOpacity, StyleSheet
} from 'react-native';
function Item({item, onLike}) {
const _onLike = React.useCallback(() => {
onLike(item.id);
}, []);
console.log('rendering', item.title);
return (
<TouchableOpacity onPress={_onLike} style={styles.item}>
<Text>{item.title} : {item.liked ? 'liked' : 'not liked'}</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
item: {
marginVertical: 10,
backgroundColor: 'white',
padding: 15,
borderWidth: 1
}
});
const areEqual = (prevProps, nextProps) => {
return prevProps.item.liked === nextProps.item.liked;
}
export default React.memo(Item, areEqual);
Related
I am trying to change the properties of objects inside of an object and
trying to add new properties to these objects but keeping the old values.
I can't find out how to get the right nested object by index, not id because
the id can be different from the .map index.
This is what I got so far, the Object names are for testing purposes
only and maybe the "updateNestedObject" can be run in the parent?
Thank you in advance and sorry if this is a noob question.
Neval
import React, { useState } from 'react';
import { View, TextInput, Text, StyleSheet, Button, Alert } from 'react-native';
function ObjectScreen() {
const [state, setState] = useState({
id: 1,
name: 'Test Object',
nested: [
{
id: 1,
title: 'Object 1',
},
{
id: 2,
title: 'Object 1',
}
]
});
function editNested({nestedObject, index, setState}) {
const updateNestedObject = () => {
setState(prevState => ({
nested: [
...prevState.nested,
[prevState.nested[index].comment]: 'Test Comment',
},
}));
}
return (
<View>
<Text>{object.title}</Text>
<TextInput
style={styles.input}
name="comment"
onChangeText={updateNestedObject}
/>
</View>
);
}
return (
<>
<Text>{state.name}</Text>
{ state.nested.map((nestedObject, key)=>{
return (
<editNested key={key} index={key} object={object} nestedObject={nestedObject}/>
)
})}
</>
)
}
const styles = StyleSheet.create({
container: {},
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
export default ObjectScreen;
There were few issues:
JSX component name editNested should start with a capital letter.
And editNested component should be on its own function, should not define inside another component which caused your TextInput to lose focus after each render cycle.
The setState call should be changed like below:
const updateNestedObject = (text) => {
setState((prevState) => ({
...prevState,
nested: prevState.nested.map((item) =>
item.id === nestedObject.id ? { ...item, value: text } : item
)
}));
};
Try the code below:
import React, { useState } from "react";
import { View, TextInput, Text, StyleSheet, Button, Alert } from "react-native";
function EditNested({ nestedObject, setState }) {
const updateNestedObject = (text) => {
setState((prevState) => ({
...prevState,
nested: prevState.nested.map((item) =>
item.id === nestedObject.id ? { ...item, value: text } : item
)
}));
};
return (
<View>
<Text>{nestedObject.title}</Text>
<TextInput
style={styles.input}
onChangeText={updateNestedObject}
value={nestedObject.value}
/>
</View>
);
}
function ObjectScreen() {
const [state, setState] = useState({
id: 1,
name: "Test Object",
nested: [
{
id: 1,
title: "Object 1",
value: ""
},
{
id: 2,
title: "Object 1",
value: ""
}
]
});
console.log(state);
return (
<>
<Text>{state.name}</Text>
{state.nested.map((nestedObject, key) => {
return (
<EditNested
key={key}
nestedObject={nestedObject}
setState={setState}
/>
);
})}
</>
);
}
const styles = StyleSheet.create({
container: {},
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10
}
});
export default ObjectScreen;
Working Demo
As per que you can update nested array with below method
const updateNestedObject = (values, item, index) => {
console.log('values', values);
const tempMainObj = state;
const tempArr = state.nested;
tempArr[index].value = values;
const updatedObj = { ...tempMainObj, nested: tempArr };
setState(updatedObj);
};
Full Example
import React, { useState } from 'react';
import { View, TextInput, Text, StyleSheet, Button, Alert } from 'react-native';
function ObjectScreen() {
const [state, setState] = useState({
id: 1,
name: 'Test Object',
nested: [
{
id: 1,
title: 'Object 1',
value: '',
},
{
id: 2,
title: 'Object 1',
value: '',
},
],
});
const updateNestedObject = (values, item, index) => {
console.log('values', values);
const tempMainObj = state;
const tempArr = state.nested;
tempArr[index].value = values;
const updatedObj = { ...tempMainObj, nested: tempArr };
setState(updatedObj);
};
return (
<>
<Text style={{ marginTop: 50 }}>{state.name}</Text>
{state.nested.map((item, index) => {
return (
<>
<Text>{item.title}</Text>
<TextInput
value={item?.value}
style={styles.input}
name="comment"
onChangeText={(values) => updateNestedObject(values, item, index)}
/>
</>
);
})}
</>
);
}
const styles = StyleSheet.create({
container: {
marginTop: 50,
},
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
export default ObjectScreen;
Snack expo: Live Example
I am using react-native-element-dropdown in react native app. It works fine with default value if set in useState but it's not work with set api response value and not selected in dropdown
import { Dropdown } from "react-native-element-dropdown";
const Profile = ({ navigation, route }) => {
const [country, setCountry] = useState("");
useEffect(() => {
getUserProfile();
}, []);
const getUserProfile = async () => {
return api
.getuserprofile(locale, authValues.user.id, authValues.token)
.then((response) => {
if (response.data.status) {
setCountry(response.data.body.user.country_id);
}
})
.catch((error) => {
//console.log(error);
});
};
return (
<SafeAreaView style={globalStyles.appContainer}>
<View style={globalStyles.inputBox}>
<Text style={globalStyles.inputLabel}>Country of Residence</Text>
<Dropdown
data={CountryData}
search
maxHeight={300}
labelField="value"
valueField="key"
placeholder="Country of Residence"
searchPlaceholder={"Search..."}
value={country}
onChange={(item) => {
setCountry(item.key);
}}
/>
</View>
</SafeAreaView>
);
};
export default Profile;
I've create an example of how to archive it on React native:
import * as React from 'react';
import {useState, useEffect} from 'react';
import { Text, View, StyleSheet } from 'react-native';
import Constants from 'expo-constants';
import { Dropdown } from 'react-native-element-dropdown';
import AntDesign from 'react-native-vector-icons/AntDesign';
export default function App() {
const [data, setData] = useState([{
key: 1,
value: 'Australia'
}, {
key: 2,
value: 'New Zeland'
}, {
key: 3,
value: 'The United State'
}]);
const [selectedValue, setSelectedValue] = useState(null);
const [isFocus, setIsFocus] = useState(false);
const getSelected = () => {
fetch('https://api.agify.io/?name=michael').then(res => {
setSelectedValue(3);
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
getSelected();
}, []);
return (
<View style={styles.container}>
<Dropdown
style={[styles.dropdown, isFocus && { borderColor: 'blue' }]}
data={data}
search
maxHeight={300}
labelField="value"
valueField="key"
placeholder={!isFocus ? 'Select item' : '...'}
searchPlaceholder="Search..."
value={selectedValue}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
onChange={item => {
setSelectedValue(item.key);
setIsFocus(false);
}}
renderLeftIcon={() => (
<AntDesign
style={styles.icon}
color={isFocus ? 'blue' : 'black'}
name="Safety"
size={20}
/>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
});
and you can check the working example from here
you used item.value insted of item.key
onChange={item => {
setValue(item.value);
setIsFocus(false);
}}
I am trying to call the addOrder function from CartScreen to function located in my action folder.
the thing is whenever I pressed the OrderNow Button, the addOrder function should be triggered. But, I'm getting error like this.
CartScreen.js
import React from 'react';
import { View, Text, FlatList, Button, StyleSheet } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import * as cartAction from '../../store/actions/cart';
import ordersAction from '../../store/actions/order';
import Colors from '../../constants/Colors';
import CartItem from '../../components/shop/CartItem';
const CartScreen = props => {
const dispatch = useDispatch();
const cartTotalAmount = useSelector(state => state.cart.totalAmount);
const cartItems = useSelector(state => {
const transformedCartItems = [];
for (const key in state.cart.items) {
transformedCartItems.push({
productId: key,
productTitle: state.cart.items[key].productTitle,
productPrice: state.cart.items[key].productPrice,
quantity: state.cart.items[key].quantity,
sum: state.cart.items[key].sum,
});
}
return transformedCartItems.sort((a, b) =>
a.productId > b.productId ? 1 : -1,
);
});
console.log('CATRITEM', cartItems);
return (
<View style={styles.screen}>
<View style={styles.summary}>
<Text style={styles.summaryText}>
Total:{' '}
<Text style={styles.amount}>${cartTotalAmount.toFixed(2)}</Text>
</Text>
<Button
color={'green'}
title="Order Now"
disabled={cartItems.length === 0}
onPress={() => {
dispatch(ordersAction.addOrder(cartItems, cartTotalAmount));
}}
/>
</View>
<FlatList
data={cartItems}
keyExtractor={item => item.productId}
renderItem={itemData => (
<CartItem
quantity={itemData.item.quantity}
title={itemData.item.productTitle}
amount={itemData.item.sum}
onRemove={() => {
dispatch(cartAction.removeFromCart(itemData.item.productId));
}}
/>
)}
/>
</View>
);
};
CartScreen.navigationOptions = {
headerTitle: 'Your Cart',
};
const styles = StyleSheet.create({
screen: {
margin: 20,
},
summary: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 20,
padding: 10,
shadowColor: 'black',
shadowOpacity: 0.26,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 8,
elevation: 5,
borderRadius: 10,
backgroundColor: 'white',
},
summaryText: {
fontSize: 18,
},
amount: {
color: Colors.primary,
},
});
export default CartScreen;
order.js (in action Folder)
export const ADD_ORDER = 'ADD_ORDER';
export const addOrder = (cartItems, totalAmount) => {
return {
type: ADD_ORDER,
orderData: {
items: cartItems,
amount: totalAmount,
},
};
};
Use the import differently and call the function as:
import { addOrder } from '../../store/actions/order';
Then call as the following:
dispatch(addOrder(cartItems, cartTotalAmount));
your import is wrong . you imported orderAction like
import ordersAction from '../../store/actions/order';
what you should do is
import * as ordersAction from '../../store/actions/order';
things should work fine after
I am a beginner in React Native, Trying to learn redux, with functional components, stuck on this error.
"Error: Actions may not have an undefined "type" property. Have you misspelled a constant?".
Creating a simple. to-do list.
My Redux.js file...
import {createStore, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import {uuid} from 'react-native-uuid';
const initialState = {
todos: [
{
id: 0,
name: 'Test ToDo 1',
completed: false,
},
{
id: 1,
name: 'Test ToDo 2',
completed: true,
},
],
};
export const store = createStore(reducer, applyMiddleware(thunk));
function reducer(state = initialState, action) {
console.log('type ' + JSON.stringify(action));
switch (action.type) {
case 'ADD-TODO':
return {...state, todos: [...state, action.payload]};
case 'TOGGLE-TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload
? {...todo, completed: !todo.completed}
: todo,
),
};
case 'DELETE-TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
default:
return state;
}
}
export const addToDoAction = (todo) => ({
type: 'ADD-TODO',
payload: todo,
});
export const toggleToDoAction = (todoId) => ({
type: 'TOGGLE-TODO',
payload: todoId,
});
export const deleteToDoAction = (todoId) => ({
type: 'DELETE-TODO',
payload: todo,
});
Here is the ToDO Input component
import React, {useState} from 'react';
import {View, TextInput, Button, Text} from 'react-native';
import {useDispatch} from 'react-redux';
import {addToDoAction} from '../redux/redux';
import uuid from 'react-native-uuid';
const ToDoInput = () => {
const [text, setText] = useState('Test');
const addToDo = useDispatch((todo) => addToDoAction(todo));
const onChangeText = (text) => {
setText(text);
};
return (
<View>
<Text>Add To Do</Text>
<TextInput
style={{height: 40, borderColor: 'gray', borderWidth: 1}}
onChangeText={(text) => onChangeText(text)}
editable={true}
value={text}
/>
<Button
title={'Add ToDo'}
onPress={() =>
addToDo({
id: uuid.v4(),
name: text,
completed: false,
})
}
/>
</View>
);
};
export default ToDoInput;
When I tap the add button, I am getting error...
"Error: Actions may not have an undefined "type" property. Have you misspelled a constant?".
This is my app.js file. contents.
import React, {useState} from 'react';
import {View, Text, SafeAreaView, StyleSheet} from 'react-native';
import {Provider} from 'react-redux';
import {store} from './redux/redux';
import ToDoInput from './components/ToDoInput';
import ToDoList from './components/ToDoList';
import AddItem from './components/AddItem';
const App = () => {
return (
<Provider store={store}>
<SafeAreaView style={{flex: 1, paddingTop: 100}}>
<View style={{flex: 1, paddingTop: 100}}>
<ToDoInput />
{/* <ToDoList /> */}
</View>
</SafeAreaView>
</Provider>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 60,
},
});
export default App;
Couldn't find a way to fix this. Please help.
Do this instead.
const ToDoInput = () => {
const dispatch = useDispatch(); <-- add this line
const addToDo = useDispatch((todo) => addToDoAction(todo)); <--- remove this line
// update the callback passed into onPress as seen below
return (
<View>
<Button
title={'Add ToDo'}
onPress={() =>
dispatch(addToDoAction({
id: uuid.v4(),
name: text,
completed: false,
})
}
/>
</View>
);
};
Here is the full code:
import * as React from 'react';
import { View, ScrollView, StyleSheet } from 'react-native';
import {
Appbar,
Searchbar,
List,
BottomNavigation,
Text,
Button,
} from 'react-native-paper';
const AccordionCollection = ({ data }) => {
var bookLists = data.map(function (item) {
var items = [];
for (let i = 0; i < item.total; i++) {
items.push(
<Button mode="contained" style={{ margin: 10 }}>
{i}
</Button>
);
}
return (
<List.Accordion
title={item.title}
left={(props) => <List.Icon {...props} icon="alpha-g-circle" />}>
<View
style={{
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
backgroundColor: 'white',
}}>
{items}
</View>
</List.Accordion>
);
});
return bookLists;
};
const MusicRoute = () => {
const DATA = [
{
key: 1,
title: 'Zain dishes',
total: 21,
},
{
key: 2,
title: 'Sides',
total: 32,
},
{
key: 3,
title: 'Drinks',
total: 53,
},
{
key: 4,
title: 'Aesserts',
total: 14,
},
];
const [data, setData] = React.useState(DATA);
const [searchQuery, setSearchQuery] = React.useState('');
const [sortAZ, setSortAZ] = React.useState(false);
const onChangeSearch = (query) => {
setSearchQuery(query);
const newData = DATA.filter((item) => {
return item.title.toLowerCase().includes(query.toLowerCase());
});
setData(newData);
};
const goSortAZ = () => {
setSortAZ(true);
setData(
data.sort((a, b) => (a.title > b.title ? 1 : b.title > a.title ? -1 : 0))
);
};
const goUnSort = () => {
setSortAZ(false);
setData(DATA);
};
return (
<View>
<Appbar.Header style={styles.appBar}>
<Appbar.BackAction onPress={() => null} />
<Searchbar
placeholder="Search"
onChangeText={onChangeSearch}
value={searchQuery}
style={styles.searchBar}
/>
<Appbar.Action
icon="sort-alphabetical-ascending"
onPress={() => goSortAZ()}
/>
<Appbar.Action icon="library-shelves" onPress={() => goUnSort()} />
</Appbar.Header>
<ScrollView>
<List.Section title="Accordions">
<AccordionCollection data={data} />
</List.Section>
</ScrollView>
</View>
);
};
const AlbumsRoute = () => <Text>Albums</Text>;
const MyComponent = () => {
const [index, setIndex] = React.useState(0);
const [routes] = React.useState([
{ key: 'music', title: 'Music', icon: 'queue-music' },
{ key: 'albums', title: 'Albums', icon: 'album' },
]);
const renderScene = BottomNavigation.SceneMap({
music: MusicRoute,
albums: AlbumsRoute,
});
return (
<BottomNavigation
navigationState={{ index, routes }}
onIndexChange={setIndex}
renderScene={renderScene}
/>
);
};
const styles = StyleSheet.create({
appBar: {
justifyContent: 'space-between',
},
searchBar: {
width: '60%',
shadowOpacity: 0,
borderRadius: 10,
backgroundColor: '#e4e3e3',
},
});
export default MyComponent;
Expo Snack Link
There are two weird mechanisms.
First
If I remove sortAZ(true) in goSortAZ() and sortAZ(false) in goUnSort(), the state data stops updating after you press on (1) sort and (2) unsort buttons more than three times.
Second
If I remove DATA array outside the component, sort and unsort buttons does not work/update.
If I do not remove these two, I can sort and unsort the list.
I feel that the code is messy although it achieves the function.
My questions is:
Why adding extra state (sortAZ) helps to update other state (data)?
Just totally remove sortAZ variable (no need to use it unless you somehow want to have a loading status, but since you are not making http requests, that's not necessary) and replace goSortAZ with the following:
Remember to clone the original array in order to create a new copy and then sort that copy.
This is working fine.
const goSortAZ = () => {
setData(
[...data].sort((a, b) => (a.title > b.title ? 1 : b.title > a.title ? -1 : 0))
);
};
i would suggest using the same technique for the unSort method too.