React native renderitems with refs to textinput - reactjs

i cant seem to get my head around this.
I want to to use a button to use .focus() on my textinput in react native.
How do i pass the ref to my function and call that method?
const [{ingredients}, dispatch] = React.useContext(AddRecipeContext);
const [edit, setEdit] = React.useState('');
const startEdit = (key: string, ref: any) => {
ref.current.focus();
setEdit((prev) => (prev === key? '':key))
}
const _renderItem = ({ item }: {item: IIngredient}) => {
var textInputRef: any = React.createRef();
return (
<View key={item.key}>
<TextInput
ref={textInputRef}
onChangeText={label => setIngredientLabel(item.key, label) }
value={item.label.toString()}/>
<Pressable onPress={() => startEdit(item.key, textInputRef)}/>
</View>
);
};
const RenderIngredients = ingredients.map((item: IIngredient) => _renderItem({item}));
return (
<View>
<ScrollView>
{RenderIngredients}
</ScrollView>
</View>
);
What is the correct way to handle refs?
Thank you in advance.
EDIT: ref.focus(); to ref.current.focus();

This should do the job
const [{ ingredients }, dispatch] = React.useContext(AddRecipeContext);
const [edit, setEdit] = React.useState("");
const _renderItem = ({ item }: { item: IIngredient }) => {
var textInputRef: any = React.createRef();
return (
<View key={item.key}>
<TextInput
ref={textInputRef}
onChangeText={(label) => setIngredientLabel(item.key, label)}
value={item.label.toString()}
/>
<Pressable
onPress={() => {
textInputRef.current.focus();
// current was missing
setEdit((prev) => (prev === item.key ? "" : item.key));
}}
/>
</View>
);
};
const RenderIngredients = ingredients.map((item: IIngredient) =>
_renderItem({ item })
);
return (
<View>
<ScrollView>{RenderIngredients}</ScrollView>
</View>
);

Your forgot the current object with the ref: ref.focus(); should be ref.current.focus();
See how to accessing refs

Related

Getting "Can't perform a React state update on an unmounted component" only the first time

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.

Unable to push data into an array - React Native

one place I seem to be stuck is on is being able to populate an array of objects, which are used for a FlatList later on.
I get this data from my FireStore – for each document, it will push the objects into ‘const DATA = []’
But when I run the ‘getUsers()’ inside of UseEffect, it only updates ‘DATA’ inside of the method, it doesn’t update the global variable.
Im new to react native, so im probably missing something important within my structure. Thanks for any assistance tho!
I need the format of DATA to look like this example:
My Code:
const MainScreen = () => {
const DATA = [];
const navigation = useNavigation()
const [selectedId, setSelectedId] = useState(null);
const usersCollectionRef = collection(db, "Posts");
const [Data, setData]= useState([]);
LogBox.ignoreLogs(['Setting a timer']);
LogBox.ignoreLogs(['AsyncStorage has been extracted']);
LogBox.ignoreAllLogs(true);
useEffect(() => {
getUsers();
console.log(DATA);
}, []);
const getUsers = async () => {
const data = await getDocs(usersCollectionRef);
data.forEach(doc =>{
const dataG = (doc.id, "=>", doc.data());
DATA.push({
id: doc.id,
title: dataG.status +" "+ dataG.Type,
status: dataG.status,
location: dataG.Location,
type: dataG.Type,
injured: dataG.Injured,
collar: dataG.Collar,
color: dataG.Colour,
username: dataG.username,
description: dataG.Description,
imagesrc: dataG.picture });
})
};
const Item = ({ item, onPress, backgroundColor, textColor }) => (
<View style={styles.ContentBox}>
<TouchableOpacity onPress={onPress} style={[styles.item, backgroundColor]}>
<Text style={[styles.title, textColor]}>{item.title}</Text>
<Text style={styles.ContentText}>By: {item.username}</Text>
</TouchableOpacity>
<Image source = {{uri: item.imagesrc}}
style = {{ width: 200, height: 200, alignSelf:'center' }}/>
<Text style={styles.ContentText}>Animal: {item.type}</Text>
<Text style={styles.ContentText}>Location: {item.location}</Text>
<Text style={styles.ContentText}>Injured: {item.injured}</Text>
<Text style={styles.ContentText}>Colour: {item.color}</Text>
<Text style={styles.ContentText}>Has a Collar: {item.collar}</Text>
<Text style={styles.ContentText}>Description: {item.description}</Text>
</View>
);
const renderItem = ({ item }) => {
const backgroundColor = item.status === "lost" ? '#b80909' : '#098231';
const color = item.id === selectedId ? 'white' : 'white';
return (
<Item
item={item}
onPress={() => setSelectedId(item.id)}
backgroundColor={{ backgroundColor }}
textColor={{ color }}
/>
);
};
const PostScreen = () =>{
navigation.navigate('PostScreen');
}
return (
<SafeAreaView style={styles.container}>
<View style={styles.MainFeed}>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
extraData={selectedId}
/>
</View>
)
instead of pushing data in a variable and then updating the state, you can do it like this directly -
setData([...Data,{
id: doc.id,
title: dataG.status +" "+ dataG.Type,
status: dataG.status,
location: dataG.Location,
type: dataG.Type,
injured: dataG.Injured,
collar: dataG.Collar,
color: dataG.Colour,
username: dataG.username,
description: dataG.Description,
imagesrc: dataG.picture
}])
The answer is that after doing DATA.push(...), I needed to setData(DATA).

Why do I get "undefined is not an object(evaluating "inputRef.current.incFont"?

It doesnt do what I want.
I have two textArea, I use them as they were button. One will increase FontSize, other will decrease it.
When I click on decreaseFont text, i get that error.
Here is the code I added,
const MyInput = React.forwardRef((props,ref) => {
const [fontSize, setFontSize] = useState(12);
React.useImperativeHandle(ref,() => {
incFont: () => {setFontSize(fontSize+2)}
decFont: () => {setFontSize(fontSize-2)}
})
return(
<TextInput style={{fontSize, borderColor:"red",borderWidth:1}} />
)
})
inside my main function:
const inputRef = useRef();
<View>
<MyInput ref={inputRef}/>
<Text onPress={()=>inputRef.current.incFont()}>IncreaseFont</Text>
<Text onPress={() => inputRef.current.decFont()}>DecreaseFont</Text>
</View>
Change useImperativeHandle like below,
React.useImperativeHandle(ref, () => ({
incFont: () => {
setFontSize(fontSize + 2);
},
decFont: () => {
setFontSize(fontSize - 2);
}
}));
working code - https://codesandbox.io/s/distracted-burnell-0h70g?file=/src/App.js

undefined is not an object(singleValue.StopTracking) on Swipe animation

I have a Delete on Swipe function that returns TypeError: undefined is not an object(evaluating 'singleValue.StopTracking')
Im using expo to run the project
After I swipe, expo returns the error, I click dismiss and the product disappears from my cart
I don't know what any more information to give
const rowTranslateAnimatedValues = {};
Array(20)
.fill('')
.forEach((_, i) => {
rowTranslateAnimatedValues[${i}] = new Animated.Value(1);
});
const [cart, setCart] = useState([]);
const [products, setProducts] = useState([]);
const [valueInput, setValueInput] = useState(1);
const [listData, setListData] = useState(
Array(4)
.fill('')
.map((_, i) => ({ key: `${i}`, text: `item #${i}` }))
);
const onSwipeValueChange = swipeData => {
const { key, value } = swipeData;
if (
value < -Dimensions.get('window').width &&
!this.animationIsRunning
) {
this.animationIsRunning = true;
Animated.timing(rowTranslateAnimatedValues[key], {
toValue: 0,
duration: 500,
})
.start(() => {
const newData = [...listData];
const prevIndex = listData.findIndex(item => item.key === key);
newData.splice(prevIndex, 1);
setListData(newData);
this.animationIsRunning = false;
});
}
};
const renderItem = data => (
<Animated.View
>
<TouchableHighlight
onPress={() => console.log('You touched me')}
style={styles.rowFront}
underlayColor={'#AAA'}
>
<View>
<Text>I am {data.item.text} in a SwipeListView</Text>
</View>
</TouchableHighlight>
</Animated.View>
);
const renderHiddenItem = () => (
<View style={styles.rowBack}>
<View style={[styles.backRightBtn, styles.backRightBtnRight]}>
<Text style={styles.backTextWhite}>Delete</Text>
</View>
</View>
);
return (
<Block flex center style={styles.cart}>
<View style={styles.container}>
<SwipeListView
disableRightSwipe
data={cart}
renderItem={renderProduct}
showsVerticalScrollIndicator={false}
keyExtractor={(item, index) => `${index}-${item.title}`}
ListEmptyComponent={renderEmpty()}
ListHeaderComponent={renderHeader()}
ListFooterComponent={renderFooter()}
renderHiddenItem={renderHiddenItem}
rightOpenValue={-Dimensions.get('window').width}
onSwipeValueChange={onSwipeValueChange}
/>
</View>
</Block>
);
}
For anyone who faces this problem,
you can just think this.animationIsRunning of a variable that manages the start and the end of animation.
So you can just use const animationIsRunning = useRef(false) to manage the animation state.
You can use animationIsRunning.current instead of this.animationIsRunning.
Also, this.animationIsRunning = true or false can be changed into animationIsRunning.current = true or false
See code at
https://github.com/jemise111/react-native-swipe-list-view/pull/521/commits/2946e3a123f26ef5292a3884e0aec58436386e18

React hook logging a useState element as null when it is not

I have a method,
const handleUpvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log('mappedPosts', mappedPosts); // null
console.log('newPosts', newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
That is attached to a mapped element,
const mapped = userPosts.map((post, index) => (
<ListItem
rightIcon = {
onPress = {
() => handleUpvote(post, index)
}
......
And I have
const [mappedPosts, setMappedPosts] = useState(null);
When the component mounts, it takes userPosts from the redux state, maps them out to a ListItem and appropriately displays it. The problem is that whenever handleUpvote() is entered, it sees mappedPosts as null and therefore sets the whole List to null at setMappedPosts(newPosts);
What am I doing wrong here? mappedPosts is indeed not null at the point when handleUpvote() is clicked because.. well how can it be, if a mappedPosts element was what invoked the handleUpvote() method in the first place?
I tried something like
setMappedPosts({
...mappedPosts,
mappedPosts[index]: post
});
But that doesn't even compile. Not sure where to go from here
Edit
Whole component:
const Profile = ({
navigation,
posts: { userPosts, loading },
auth: { user, isAuthenticated },
fetchMedia,
checkAuth,
upvote,
downvote
}) => {
const { navigate, replace, popToTop } = navigation;
const [mappedPosts, setMappedPosts] = useState(null);
useEffect(() => {
if (userPosts) {
userPosts.forEach((post, index) => {
post.userAction = null;
post.likes.forEach(like => {
if (like._id.toString() === user.id) {
post.userAction = "liked";
}
});
post.dislikes.forEach(dislike => {
if (dislike._id.toString() === user.id) {
post.userAction = "disliked";
}
});
});
const mapped = userPosts.map((post, index) => (
<ListItem
Component={TouchableScale}
friction={100}
tension={100}
activeScale={0.95}
key={index}
title={post.title}
bottomDivider={true}
rightIcon={
<View>
<View style={{ flexDirection: "row", justifyContent: "center" }}>
<Icon
name="md-arrow-up"
type="ionicon"
color={post.userAction === "liked" ? "#a45151" : "#517fa4"}
onPress={() => handleUpvote(post, index)}
/>
<View style={{ marginLeft: 10, marginRight: 10 }}>
<Text>{post.likes.length - post.dislikes.length}</Text>
</View>
<Icon
name="md-arrow-down"
type="ionicon"
color={post.userAction === "disliked" ? "#8751a4" : "#517fa4"}
onPress={() => handleDownvote(post, index)}
/>
</View>
<View style={{ flexDirection: "row" }}>
<Text>{post.comments.length} comments</Text>
</View>
</View>
}
leftIcon={
<View style={{ height: 50, width: 50 }}>
<ImagePlaceholder
src={post.image.location}
placeholder={post.image.location}
duration={1000}
showActivityIndicator={true}
activityIndicatorProps={{
size: "large",
color: index % 2 === 0 ? "blue" : "red"
}}
/>
</View>
}
></ListItem>
));
setMappedPosts(mapped);
} else {
checkAuth();
fetchMedia();
}
}, [userPosts, mappedPosts]);
const handleDownvote = (post, index) => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
if (post.userAction === "dislike") {
newPosts.userAction = null;
} else {
newPosts.userAction = "dislike";
}
setMappedPosts(newPosts);
downvote(user.id, post._id);
};
const handleUpvote = post => {
let newPosts = JSON.parse(JSON.stringify(mappedPosts));
console.log("mappedPosts", mappedPosts); // null
console.log("newPosts", newPosts); // null
if (post.userAction === "like") {
newPosts.userAction = null;
} else {
newPosts.userAction = "like";
}
setMappedPosts(newPosts);
upvote(user.id, post._id);
};
return mappedPosts === null ? (
<Spinner />
) : (
<ScrollView
refreshControl={
<RefreshControl
refreshing={false}
onRefresh={() => {
this.refreshing = true;
fetchMedia();
this.refreshing = false;
}}
/>
}
>
{mappedPosts}
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center"
}
});
Profile.propTypes = {
auth: PropTypes.object.isRequired,
posts: PropTypes.object.isRequired,
fetchMedia: PropTypes.func.isRequired,
checkAuth: PropTypes.func.isRequired,
upvote: PropTypes.func.isRequired,
downvote: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
auth: state.auth,
posts: state.posts
});
export default connect(
mapStateToProps,
{ fetchMedia, checkAuth, upvote, downvote }
)(Profile);
The reason why your current solution doesn't work is because you're rendering userPosts inside of the useEffect hook, which looks like it only runs once, ends up "caching" the initial state, and that's what you end up seeing in your handlers.
You will need to use multiple hooks to get this working properly:
const Profile = (props) => {
// ...
const [mappedPosts, setMappedPosts] = useState(null)
const [renderedPosts, setRenderedPosts] = useState(null)
useEffect(() => {
if (props.userPosts) {
const userPosts = props.userPosts.map(post => {
post.userAction = null;
// ...
})
setMappedPosts(userPosts)
} else {
checkAuth()
fetchMedia()
}
}, [props.userPosts])
const handleDownvote = (post, index) => {
// ...
setMappedPosts(newPosts)
}
const handleUpvote = (post) => {
// ...
setMappedPosts(newPosts)
}
useEffect(() => {
if (!mappedPosts) {
return
}
const renderedPosts = mappedPosts.map((post, index) => {
return (...)
})
setRenderedPosts(renderedPosts)
}, [mappedPosts])
return !renderedPosts ? null : (...)
}
Here's a simplified example that does what you're trying to do:
CodeSandbox
Also, one note, don't do this:
const Profile = (props) => {
const [mappedPosts, setMappedPosts] = useState(null)
useEffect(() => {
if (userPosts) {
setMappedPosts() // DON'T DO THIS!
} else {
// ...
}
}, [userPosts, mappedPosts])
}
Stay away from updating a piece of state inside of a hook that has it in its dependency array. You will run into an infinite loop which will cause your component to keep re-rendering until it crashes.
Let me use a simplified example to explain the problem:
const Example = props => {
const { components_raw } = props;
const [components, setComponents] = useState([]);
const logComponents = () => console.log(components);
useEffect(() => {
// At this point logComponents is equivalent to
// logComponents = () => console.log([])
const components_new = components_raw.map(_ => (
<div onClick={logComponents} />
));
setComponents(components_new);
}, [components_raw]);
return components;
};
As you can see the cycle in which setComponents is called, components is empty []. Once the state is assigned, it stays with the value logComponents had, it doesn't matter if it changes in a future cycle.
To solve it you could modify the necessary element from the received data, no components. Then add the onClick on the return in render.
const Example = props => {
const { data_raw } = props;
const [data, setData] = useState([]);
const logData = () => console.log(data);
useEffect(() => {
const data_new = data_raw.map(data_el => ({
...data_el // Any transformation you need to do to the raw data.
}));
setData(data_new);
}, [data_raw]);
return data.map(data_el => <div {...data_el} onClick={logData} />);
};

Resources