Related
I'm trying to keep track of a list of chosen exercises, and then render 'added' if the chosen exercise is in the list, or 'not added' if it's not in the list. I add the exercise to chosenExerciseArray when it's not already included, and remove it if it is. When I use console.log(chosenExerciseArray) it correctly displays the list, but if I use setChosenExercises(chosenExerciseArray) the state does not get updated correctly, and the chosenExerciseArray somehow stops logging the correct array as well.
I've also tried adding chosenExerciseArray as a useEffect dependency, but that causes an infinite loop. I'm really not sure what's going on here, but I believe it's an issue with my understanding of state.
EDIT: The TouchableOpacity is inside of a map, which may also be the issue, but I'm not sure how
<View style={styles.exerciseheadericongroup}>
<TouchableOpacity
onPress={() => {
if(!chosenExerciseArray.includes(exercise.id)) {
chosenExerciseArray.push(exercise.id);
} else {
for(var i=0; i<chosenExerciseArray.length; i++) {
if(chosenExerciseArray[i] === exercise.id) {
chosenExerciseArray.splice(i, 1);
}
}
}
console.log(chosenExerciseArray);
}}
>
{chosenExercises.includes(exercise.id) ? (
<Text>added</Text>
) : (
<Text>not added</Text>
)}
</TouchableOpacity>
</View>
As stated in the React documentation
state is a reference to the component state at the time the change is being applied. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from state and props. [link]
I wish I could explain it better, but basically it means that manipulating chosenExerciseArray with .push and then executing setChosenExerciseArray(chosenExerciseArray) messes up the state handling. You need to make a copy of chosenExerciseArray and manipulate it instead, and then use the manipulated copy to set the new state. I have created two examples for you to look at:
Example 1 uses a different logic, where added is part of the exercise object itself, such that that the chosenExerciseArray is not necessary to keep track of whether the exercise is added or not. This is also a bit more efficient, because you do not need to check if an exercise is in the chosenExerciseArray or not.
import React, { useState } from "react";
import { Pressable, StyleSheet } from "react-native";
import { Text, View } from "../components/Themed";
interface Exercise {
id: string;
name: string;
added: boolean;
}
const updateExerciseArray = (exercises: Exercise[], exercise: Exercise) => {
// Shallow copy
const updatedExercises = [...exercises];
// Manipulate the copy
return updatedExercises.map((updatedExercise) => {
if (updatedExercise.id === exercise.id) {
// Shallow copy of object in array where added is negated
return { ...updatedExercise, added: !updatedExercise.added };
}
return updatedExercise;
});
};
export default function Example1() {
const [exercises, setExercises] = useState<Exercise[]>([
{ id: "1", name: "Pushups", added: false },
{ id: "2", name: "Situps", added: false },
{ id: "3", name: "Squats", added: false },
{ id: "4", name: "Pullups", added: false },
{ id: "5", name: "Leg Raises", added: false },
{ id: "6", name: "Plank", added: false },
{ id: "7", name: "Burpees", added: false },
{ id: "8", name: "Jumping Jacks", added: false },
{ id: "9", name: "Wall Sit", added: false },
]);
return (
<View style={styles.container}>
{exercises.map((exercise) => {
return (
<Pressable
key={exercise.id}
onPress={() => {
setExercises(updateExerciseArray(exercises, exercise));
}}
style={styles.row}
>
<Text style={styles.text}>{exercise.name}</Text>
<Text style={styles.text}>
{exercise.added ? "Added" : "Not added"}
</Text>
</Pressable>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 50,
flex: 1,
backgroundColor: "#fff",
justifyContent: "center",
},
row: {
marginVertical: 10,
flexDirection: "row",
justifyContent: "space-between",
},
text: {
fontSize: 20,
},
});
Example 2 uses a solution that fits your current implementation.
import React, { useState } from "react";
import { Pressable, StyleSheet } from "react-native";
import { Text, View } from "../components/Themed";
interface Exercise {
id: string;
name: string;
}
const updateExerciseArray = (
chosenExerciseArray: string[],
exerciseToChange: Exercise
) => {
// Shallow copy
const updatedChosenExerciseArray = [...chosenExerciseArray];
if (updatedChosenExerciseArray.includes(exerciseToChange.id)) {
// Remove the exercise from the array
return updatedChosenExerciseArray.filter(
(exerciseId) => exerciseId !== exerciseToChange.id
);
} else {
// Append the exercise to the array
return [...updatedChosenExerciseArray, exerciseToChange.id];
}
};
export default function Example2() {
const [exercises] = useState<Exercise[]>([
{ id: "1", name: "Pushups" },
{ id: "2", name: "Situps" },
{ id: "3", name: "Squats" },
{ id: "4", name: "Pullups" },
{ id: "5", name: "Leg Raises" },
{ id: "6", name: "Plank" },
{ id: "7", name: "Burpees" },
{ id: "8", name: "Jumping Jacks" },
{ id: "9", name: "Wall Sit" },
]);
const [chosenExerciseArray, setChosenExerciseArray] = useState<string[]>([
"1",
"2",
]);
return (
<View style={styles.container}>
{exercises.map((exercise) => {
return (
<Pressable
key={exercise.id}
onPress={() => {
setChosenExerciseArray(
updateExerciseArray(chosenExerciseArray, exercise)
);
}}
style={styles.row}
>
<Text style={styles.text}>{exercise.name}</Text>
<Text style={styles.text}>
{chosenExerciseArray.includes(exercise.id)
? "Added"
: "Not added"}
</Text>
</Pressable>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 50,
flex: 1,
backgroundColor: "#fff",
justifyContent: "center",
},
row: {
paddingVertical: 10,
flexDirection: "row",
justifyContent: "space-between",
},
text: {
fontSize: 20,
},
});
I have seen a lot of answers for FlatList not scrolling for React Native, but none of the answers seem to work.
The answer usually says to add flex: 1 to the parent, but here is an example that has that and it still doesn't scroll.
You can test here, but make sure you select iOS or Android. It does scroll for web.
import React from 'react';
import { SafeAreaView, View, FlatList, StyleSheet, Text, StatusBar } from 'react-native';
const DATA = [
{
id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
title: "First Item",
},
{
id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
title: "Second Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d72",
title: "Third Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d73",
title: "Fourth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d74",
title: "Fifth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d75",
title: "Sixth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d76",
title: "Seventh Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d77",
title: "Eighth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d77",
title: "Ninth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d77",
title: "Tenth Item",
},
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
const renderItem = ({ item }) => (
<Item title={item.title} />
);
return (
<SafeAreaView style={styles.container}>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
},
title: {
fontSize: 32,
},
});
export default App;
Modify the SafeAreaView to be within it's own container and added a View container with flexGrow: 1 instead of flex: 1:
const App = () => {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.container}>
<FlatList
data={data}
renderItem={({ item }) => (<Item title={item.title} />) }
keyExtractor={item => item.id}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: {
marginTop: StatusBar.currentHeight || 0,
},
container: {
flexGrow: 1
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
},
title: {
fontSize: 32,
},
});
Above worked in a snack Expo testing for iOS. Entire sample code with a little re-write:
import React from 'react';
import { SafeAreaView, View, FlatList, StyleSheet, Text, StatusBar } from 'react-native';
const data = [
{
id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
title: "First Item",
},
{
id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
title: "Second Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d72",
title: "Third Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d73",
title: "Fourth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d74",
title: "Fifth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d75",
title: "Sixth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d76",
title: "Seventh Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d77",
title: "Eighth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d77",
title: "Ninth Item",
},
{
id: "58694a0f-3da1-471f-bd96-145571e29d77",
title: "Tenth Item",
},
];
const Item = ({ title }) => (
<View style={styles.item}>
<Text style={styles.title}>{title}</Text>
</View>
);
const App = () => {
return (
<SafeAreaView style={styles.safe}>
<View style={styles.container}>
<FlatList
data={data}
renderItem={({ item }) => (<Item title={item.title} />) }
keyExtractor={item => item.id}
/>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: {
marginTop: StatusBar.currentHeight || 0,
},
container: {
flexGrow: 1
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
},
title: {
fontSize: 32,
},
});
export default App
export default class App extends Component {
constructor() {
super();
// flatlist data
this.state = {
data: [
{ key: "Skptricks", data: "one", name: "one" },
{ key: "Sumit" , data: "two" , name: "one"},
{ key: "Amit" , data: "three", name: "one"},
],
}
}
// flat list
render() {
return (
<View>
<FlatList
data={this.state.data}
renderItem={({item}) => <Text>{item.name}</Text>
/>
</View>
);
}
}
Can you explain what exactly do you need to print ? Please refer the code. I'm printing the 'name' item of your array. If you want to print 'data' items , use item.data inside renderItem. Let me know if you need any help.
const DATA = [{ key: "Skptricks", data: "one", name: "one" },
{ key: "Sumit" , data: "two" , name: "one"},
{ key: "Amit" , data: "three", name: "one"},
];
const Item = ({ name }) => (
<View style={styles.item}>
<Text style={styles.name}>{name}</Text>
</View>
);
const App = () => {
const renderItem = ({ item }) => (
<Item name={item.name} />
);
return (
<SafeAreaView style={styles.container}>
<FlatList
data={DATA}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: StatusBar.currentHeight || 0,
},
item: {
backgroundColor: '#f9c2ff',
padding: 20,
marginVertical: 8,
marginHorizontal: 16,
},
name: {
fontSize: 32,
},
});
I'm working on separating code from index.tsx into two different files viz: firstTab.tsx and secondTab.tsx. I haven't started working on secondTab.tsx yet.
I separated first tab related code into firstTab.tsx as shown in the following code editor: The full functional code with both tabs working are in index.tsx is pasted below:
import React, { Component } from "react";
import { render } from "react-dom";
import "jqwidgets-scripts/jqwidgets/styles/jqx.base.css";
import JqxButton from "jqwidgets-scripts/jqwidgets-react-tsx/jqxbuttons";
import * as ReactDOM from "react-dom";
import JqxWindow from "jqwidgets-scripts/jqwidgets-react-tsx/jqxwindow";
import JqxInput from "jqwidgets-scripts/jqwidgets-react-tsx/jqxinput";
import JqxChart, {
IChartProps
} from "jqwidgets-scripts/jqwidgets-react-tsx/jqxchart";
import JqxGrid, {
IGridProps,
jqx
} from "jqwidgets-scripts/jqwidgets-react-tsx/jqxgrid";
import JqxTabs from "jqwidgets-scripts/jqwidgets-react-tsx/jqxtabs";
import JqxDropDownList, {
IDropDownListProps
} from "jqwidgets-scripts/jqwidgets-react-tsx/jqxdropdownlist";
import firstTab from './firstTab';
interface AppProps {}
interface AppState {
name: string;
}
interface IProps extends IGridProps {
dropdownlistSource: IDropDownListProps["source"];
}
class App extends Component<{}, IProps> {
private myTabs = React.createRef<JqxTabs>();
private gridElement = React.createRef<HTMLDivElement>();
private myGrid = React.createRef<JqxGrid>();
private gridElementTwo = React.createRef<HTMLDivElement>();
private myGrid2 = React.createRef<JqxGrid>();
constructor(props: {}) {
super(props);
this.state = {
dropdownlistSource: [
{ value: 0, label: "Affogato" },
{ value: 1, label: "Americano" },
{ value: 2, label: "Bicerin" },
{ value: 3, label: "Breve" }
]
};
}
public render() {
return (
<JqxTabs
ref={this.myTabs}
// #ts-ignore
width={400}
height={560}
initTabContent={this.initWidgets}
>
<ul>
<li style={{ marginLeft: 30 }}>
<div style={{ height: 20, marginTop: 5 }}>
<div
style={{
marginLeft: 4,
verticalAlign: "middle",
textAlign: "center",
float: "left"
}}
>
US Indexes
</div>
</div>
</li>
<li>
<div style={{ height: 20, marginTop: 5 }}>
<div
style={{
marginLeft: 4,
verticalAlign: "middle",
textAlign: "center",
float: "left"
}}
>
NASDAQ compared to S&P 500
</div>
</div>
</li>
</ul>
<div style={{ overflow: "hidden" }}>
<div id="jqxGrid" ref={this.gridElement} />
<div style={{ marginTop: 10, height: "15%" }} />
</div>
<div style={{ overflow: "hidden" }}>
<div id="jqxGrid2" ref={this.gridElementTwo} />
<div style={{ marginTop: 10, height: "15%" }} />
</div>
</JqxTabs>
);
}
private initGrid = () => {
const source = {
datafields: [{ name: "Date" }, { name: "S&P 500" }, { name: "NASDAQ" }],
datatype: "csv",
localdata: `1/2/2014,1831.98,4143.07
1/3/2014,1831.37,4131.91
1/6/2014,1826.77,4113.68
1/7/2014,1837.88,4153.18
1/8/2014,1837.49,4165.61
1/9/2014,1838.13,4156.19
2/6/2014,1773.43,4057.12
2/7/2014,1797.02,4125.86`
};
const dataAdapter = new jqx.dataAdapter(source, {
async: false,
loadError: (xhr: any, status: any, error: any) => {
console.log(xhr, status, error);
}
});
const columns: IGridProps["columns"] = [
{ cellsformat: "d", datafield: "Date", text: "Date", width: 250 },
{ datafield: "S&P 500", text: "S&P 500", width: 150 },
{ datafield: "NASDAQ", text: "NASDAQ" }
];
const grid = (
<JqxGrid
ref={this.myGrid}
width={"100%"}
height={400}
source={dataAdapter}
columns={columns}
/>
);
render(grid, this.gridElement.current!);
};
private initGrid2 = () => {
const source = {
datafields: [{ name: "Date" }, { name: "S&P 500" }, { name: "NASDAQ" }],
datatype: "csv",
localdata: `1/2/2014,1831.98,4143.07
1/3/2014,1831.37,4131.91
1/6/2014,1826.77,4113.68
1/7/2014,1837.88,4153.18
1/8/2014,1837.49,4165.61
1/9/2014,1838.13,4156.19
1/10/2014,1842.37,4174.67
2/7/2014,1797.02,4125.86`
};
const dataAdapter = new jqx.dataAdapter(source, {
async: false,
loadError: (xhr: any, status: any, error: any) => {
console.log(xhr, status, error);
}
});
const columns: IGridProps["columns"] = [
{ cellsformat: "d", datafield: "Date", text: "Date", width: 250 },
{ datafield: "S&P 500", text: "S&P 500", width: 150 },
{ datafield: "NASDAQ", text: "NASDAQ" }
];
const grid = (
<JqxGrid
ref={this.myGrid2}
width={"100%"}
height={400}
source={dataAdapter}
columns={columns}
/>
);
render(grid, this.gridElementTwo.current!);
};
private initWidgets = (tab: any) => {
switch (tab) {
case 0:
this.initGrid();
break;
case 1:
this.initGrid2();
break;
}
};
}
render(<App />, document.getElementById("root"));
Question:
Since I've already moved private initGrid = () => { inside a separate file firstTab.tsx, in index.tsx where should I put {firstTab.tsx} to make sure both tabs in index.tsx works fine? I mean, even if I comment out private initGrid = () => { function from index.tsx both tabs should work fine.
Thanks
If I would refactor this I would consider the next approach:
Create Parent component Table (probably some more appropriate name)
Create a component for US Indexes
Create a component for NASDAQ compared to S&P 500
Based on the active tab render the proper component.
You could also create a separate file that contains only exports with your data.
If you then import that into your files with the functions you can use that there, keeps it cleaner.
And if you pass that data as a prop / param to your initGrid() functions, you don't have to repeat that code, can reuse it.
This is my only component:
import React from 'react';
import { View, ListView, StyleSheet, Text } from 'react-native';
import Row from './row'
import TimerMixin from 'react-timer-mixin';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20,
},
});
class ListViewDemo extends React.Component {
updateState(){
console.log("stae updation")
}
componentDidMount() {
console.log("mount");
//timer.setTimeout(this, this.updateState, 1000);
}
componentWillUnmount() {
console.log("unmount");
//timer.clearTimeout(this);
}
constructor(props) {
super(props);
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
let data = [{
"name": {
"symbol": "Hello",
"tick": 1
}},
{
"name": {
"symbol": "World",
"tick": 0
}}]
this.state = {
dataSource: ds.cloneWithRows(data),
};
}
render() {
return (
<ListView
style={styles.container}
dataSource={this.state.dataSource}
renderRow={(data) => <Row {...data} />}
/>
);
}
}
export default ListViewDemo;
Now, I want to update each of the row's ticker by incrementing it by 1 inside a timer every second. Couple of issues I'm facing is binding a function that can update the state, and getting a mixin library working for es6. Any help, please?
This is a working example where info is updated in every row after pressing Start and that uses TimerMixin.
I used some extra vars on state to make this work quickly.
It might be possible to extract data directly from dataSource (without using the updatedData var) but I haven't discovered that yet.
import React from 'react';
import { View, ListView, StyleSheet, Text, TouchableHighlight } from 'react-native';
import TimerMixin from 'react-timer-mixin';
const styles = StyleSheet.create({
container: {
flex: 1,
marginTop: 20,
},
button: {
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderRadius: 5,
padding: 5,
borderColor: 'black',
marginTop: 10,
height: 40
},
buttonText: {
flex: 1,
alignSelf: 'center',
fontSize: 20
}
});
class ListViewDemo extends React.Component {
constructor(props) {
super(props);
const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
let data = [{
"name": {
"symbol": "Hello",
"tick": 1
}},
{
"name": {
"symbol": "World",
"tick": 0
}}]
this.state = {
timerStarted: false,
updatedData: data,
dataSource: ds.cloneWithRows(data),
};
}
onTimerStart() {
if (this.state.timerStarted) {
this.setState({timerStarted: false});
return TimerMixin.clearInterval(this.state.timer);
}
var timer = TimerMixin.setInterval(
() => {
data = [{
"name": {
"symbol": "Hello",
"tick": this.state.updatedData[0].name.tick+1
}},
{
"name": {
"symbol": "World",
"tick": this.state.updatedData[1].name.tick+1
}}]
this.setState({
timerStarted: true,
updatedData: data,
dataSource: this.state.dataSource.cloneWithRows(data)
})
}, 1000
)
this.setState({timer: timer});
}
render() {
return (
<View>
<ListView
dataSource={this.state.dataSource}
renderRow={(rowData) => <Text> {rowData.name.symbol} {rowData.name.tick}</Text> }
/>
<TouchableHighlight style={styles.button}
underlayColor={'gray'}
onPress={this.onTimerStart.bind(this)}>
<Text style={styles.buttonText}>Start</Text>
</TouchableHighlight>
</View>
);
}
}
export default ListViewDemo;