I have a class used to render a list of users from a database
export default class Users extends React.Component {
constructor() {
super()
this.state = {
data : [] //define a state
}
}
renderUsers = () => {
useEffect(() => {
fetch('exemple.com')
.then((response) => response.json())
.then((json) => this.setState({data: json.result})) // set returned values into the data state
.catch((error) => console.error(error))
}, []);
return this.state.data.map((value,key)=>{ // map state and return some views
......
})
}
render() {
return (
<View style={{ flex: 1 }}>
{this.renderUsers()} //render results
</View>
);
}
}
The problem is this code will throw the following error :
Invalid Hook call, Hooks can be called only inside of the body
component
I think is not possible to use hooks inside class component..
If it's not possible what is the best approach to fetch data from server inside this class ?
You cannot use hooks in a class component. Use componentDidMount instead.
Hooks can be used only in functional components.
You could rewrite your component to be a functional one like so:
export default Users = () => {
const [data, setData] = useState([]);
useEffect(() => {
fetch('exemple.com')
.then((response) => response.json())
.then((json) => setData(json.result)) // set returned values into the data state
.catch((error) => console.error(error))
}, []);
const renderUsers = () => {
return data.map((value, key) => {
// DO STUFF HERE
})
}
return (
<View style={{ flex: 1 }}>
{renderUsers()} //render results
</View>
)
}
Related
in react native app,
i'm trying to get data from async function which will bring me back Promise<AlarmType[] | undefined>
Q1. so, in getAlarms.then() function, the undefined case is filtered and an empty array is printed in my console.
and after saving code in vscode, the console prints an array with proper data
Q2.the reason why i use useLayoutEffect and useEffect separately is
i just wanna separate the data fetching code from the react navigation header setOption code
but i'm not sure if it is a good practice
Is there any better ways to do this?
edit: i’m using react-native-simple-alarm
const [alarms, setAlarms] = useState<AlarmType[]>([]);
const fetchData = useCallback(() => {
getAlarms().then(response => {
if (response) setAlarms(response);
else console.log('undefined | empty array returned');
});
}, []);
useLayoutEffect(() => {
fetchData();
const willFocusSubscription = navigation.addListener('focus', () => {
fetchData();
});
console.log(alarms) // here, this function is called twice, and return empty array
return willFocusSubscription;
}, []);
useEffect(() => {
navigation.setOptions({
headerLeft: () => <Icon name="trash-can-outline" size={30}
onPress={() => {
deleteAllAlarms();
fetchData();
}}/>,
headerTitle: 'Alarm',
headerRight: () =><Icon name="plus" size={30} onPress={() => navigation.navigate('ModalStackView')}/>,
});
}, []);
in getAlarms.ts
export const getAlarms = async () => {
try {
return await RNGetAlarms();
} catch (error) {
console.log('setting call error' + error);
}
};
The useLayoutEffect is called before the render cycle of React which means before rendering the JSX content in your code this hook is being called.
So, If there is any requirement before JSX render like change header name, show header left or right buttons, etc.
and the useEffect is called after the initial render cycle is completed. when the JSX code is done with the rendering UI part.
So, I think your code should look like below:
const [alarms, setAlarms] = useState<AlarmType[]>([]);
const fetchData = useCallback(() => {
getAlarms().then(response => {
if (response) setAlarms(response);
else console.log('undefined | empty array returned');
});
}, []);
useEffect(() => {
const willFocusSubscription = navigation.addListener('focus', () => {
fetchData();
});
return willFocusSubscription;
}, [fetchData, navigation]);
useLayoutEffect(() => {
navigation.setOptions({
headerLeft: () => <Icon name="trash-can-outline" size={30}
onPress={() => {
deleteAllAlarms();
fetchData();
}}/>,
headerTitle: 'Alarm',
headerRight: () =><Icon name="plus" size={30} onPress={() => navigation.navigate('ModalStackView')}/>,
});
}, [deleteAllAlarms, fetchData, navigation]);
I have a reusable SearchHeader component that has a controlled input and passes the data from the input to his parents
SearchHeader.js
const SearchHeader = ({ onChangeSearch }) => {
const [searchValue, setSearchValue] = useState('');
useEffect(() => {
const timeoutId = setTimeout(() => {
onChangeSearch(searchValue);
}, 400);
return () => {
clearTimeout(timeoutId);
};
}, [searchValue, onChangeSearch]);
return (
<Container>
<ContainerHead>
<ContainerInput>
<IoSearch />
<div style={{ width: '5px' }}></div>
<TextInput
className="TextInput_Search"
placeholder="Search here..."
value={searchValue}
onChange={(evt) => setSearchValue(evt.target.value)}
/>
</ContainerInput>
<div style={{ width: '8px' }}></div>
<Text
size="12px"
weight="400"
color={Token.color.white}
cursor="pointer"
onClick={() => history.goBack()}
>
Batal
</Text>
</ContainerHead>
</Container>
);
};
Everything it's okay when I use this component in-class component to make an API request and set the state for the result
UniversalSearch.js
class UniversalSearch extends Component {
state = {
searchResult: [],
};
handleSearch = (value) => {
if (value) {
universalSearch(value).then((response) => {
this.setState({ searchResult: response.data });
console.log(response.data);
});
}
};
render() {
return (
<div>
<SearchHeader onChangeSearch={this.handleSearch} />
</div>
);
}
}
but when I turn the component into a functional component the handle search won't stop fetching an API request
export default function UniversalSearch() {
const [searchResult, setSearchResult] = useState([]);
const handlesearch = (value) => {
universalSearch(value).then((response) => {
setSearchResult(response.data)
console.log(response.data);
});
return (
<div>
<SearchHeader onChangeValue={handlesearch}/>
</div>
);
}
the infinite fetching only came when I set the state for the searchResult. but in-class component everything working as expected that make me so confused how it is possible?
This seems to happen because every time you set the state value in the function the component renders and creates a new function.
In order to fix this, you can make use of useCallback.
const handlesearch = useCallback((value) => {
universalSearch(value).then((response) => {
setSearchResult(response.data)
console.log(response.data);
}), [universalSearch])
I am new to React Native and practicing by creating a project which makes requests from a COVID-19 API. However, when I run my code, I get this error:
TypeError: undefined is not an object (evaluating 'this.state.data.confirmed.value')
import React from 'react';
import { View, Text } from 'react-native';
class TrackerScreen extends React.Component {
state = {
data: ''
}
componentDidMount = () => {
fetch('https://covid19.mathdro.id/api', {
method: 'GET'
})
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson);
this.setState({
data: responseJson
})
})
.catch((error) => {
console.error(error);
});
}
render() {
return (
<View>
<Text>
{this.state.data.confirmed.value}
</Text>
</View>
)
}
}
export default TrackerScreen;
I converted my componentDidMount to an arrow function as suggested by other members on an old thread but that did not get rid of the error. Does anybody have a solution to this issue? Any help would be appreciated. Thank you!
The first render will happen before the componentDidMount will be called. So, during the first render the data property from your state will be an empty string. And the code inside the render function is trying to access a nested prop. Either provide a more described state, like:
state = {
data: { confirmed: { value: null } }
}
or check the value inside the render function:
import React from 'react';
import { View, Text } from 'react-native';
class TrackerScreen extends React.Component {
state = {
data: ''
}
componentDidMount = () => {
fetch('https://covid19.mathdro.id/api', {
method: 'GET'
})
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson);
this.setState({
data: responseJson
})
})
.catch((error) => {
console.error(error);
});
}
render() {
const { data: {confirmed: { value } = { value: null } } } = this.state
return value ? (
<View>
<Text>
{value}
</Text>
</View>
): null;
}
}
export default TrackerScreen;
You probably don't want to render anything if the data isn't present at the moment, so an additional check for the value will handle that
I'm just getting started with React. As a simple exercise, I wanted to create some components for viewing data retrieved from the JsonMonk API. The API contains 83 user records and serves them in pages of 10.
I am trying to develop a component for viewing a list of users one page at a time which I called UserList. The code for it is below:
class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
pageNumber: 1,
users: [],
};
this.onPageNext = this.onPageNext.bind(this);
}
componentDidMount() {
this.fetchUsers(this.state.pageNumber)
.then((users) => this.setState({users: users}));
}
async fetchUsers(pageNumber) {
const response = await fetch(`https://jsonmonk.com/api/v1/users?page=${pageNumber}`);
const jsonResponse = await response.json();
return jsonResponse.data.records;
}
onPageNext() {
// ...
}
render() {
const postElements = this.state.users.map(
(props) => <User key={props._id} {...props} />);
return (
<div>
{postElements}
<div>
<button onClick={this.onPageNext}>Next</button>
</div>
</div>
);
}
}
The problem I am having pertains to the onPageNext method of my component. When the user clicks the "Next" button, I want to make a fetch for the next page of data and update the list.
My first attempt used an asynchronous arrow function passed to setState like so:
onPageNext() {
this.setState(async (state, props) => {
const nextPageNumber = state.pageNumber + 1;
const users = await this.fetchUsers(nextPageNumber);
return {pageNumber: nextPageNumber, users: users}
})
}
However, it does not seem React supports this behavior because the state is never updated.
Next, I tried to use promise .then syntax like so:
onPageNext() {
const nextPageNumber = this.state.pageNumber + 1;
this.fetchUsers(nextPageNumber)
.then((users) => this.setState({pageNumber: nextPageNumber, users: users}));
}
This works but the problem here is that I am accessing the class's state directly and not through setState's argument so I may receive an incorrect value. Say the user clicks the "Next" button three times quickly, they may not advance three pages.
I have essentially run into a chicken-or-the-egg type problem. I need to pass a callback to setState but I need to know the next page ID to fetch the data which requires calling setState. After studying the docs, I feel like the solution is moving the fetch logic out of the UsersList component, but I'm not entirely sure how to attack it.
As always, any help is appreciated.
You need to change onPageNext as below:
onPageNext() {
this.setState( prevState => {
return {pageNumber: prevState.pageNumber + 1}
}, () =>{
this.fetchUsers(this.state.pageNumber).then(users => this.setState({users: users}) )
});
}
Here is the Complete Code:
import React from "react";
export default class UserList extends React.Component {
constructor(props) {
super(props);
this.state = {
pageNumber: 1,
users: [],
};
this.onPageNext = this.onPageNext.bind(this);
}
componentDidMount() {
this.fetchUsers(this.state.pageNumber)
.then((users) => {
console.log(users, 'users');
this.setState({users: users})
}
);
}
async fetchUsers(pageNumber) {
const response = await fetch(`https://jsonmonk.com/api/v1/users?page=${pageNumber}`);
const jsonResponse = await response.json();
return jsonResponse.data.records;
}
onPageNext() {
this.setState( prevState => {
return {pageNumber: prevState.pageNumber + 1}
}, () =>{
this.fetchUsers(this.state.pageNumber).then(users => this.setState({users: users}) )
});
}
render() {
const postElements = this.state.users.map(
(user) => <User key={user._id} {...user} />);
return (
<div>
{postElements}
<div>
<button onClick={this.onPageNext}>Next</button>
</div>
</div>
);
}
}
function User(props) {
return (
<div>
<div style={{padding: 5}}>Name: {props.first_name} {props.last_name}</div>
<div style={{padding: 5}}>Email: {props.email}</div>
<div style={{padding: 5}}>Phone: {props.mobile_no}</div>
<hr/>
</div>
);
}
Here is the Code Sandbox
I have an app where a user is prompted to enter a code, it calls an external service and returns data from the api. Currently I'm able to enter the code and call the api and get a response back, I can also poll it every 10 seconds and return the data (no sockets on api service).
I have two components, showLanding and showResult.
When the app initialises, I want to show showLanding, prompt for a code and return showResult.
The problem I'm having is I'm not able to get it to 'update' the view.
I have tried ReactDOM.render or render() within the getScreen function, I feel as though I've missed something glaringly obvious when reading the React Native doco.
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {showLanding: true, showResult: false}
}
render() {
if (this.state.showLanding) {
return (
<View
tapCode={this.state.tapCode}
style={styles.appContainer}>
{this.state.showLanding && <LandingComponent/>}
</View>
);
}
if (this.state.showResult) {
return (
<View
tapCode={this.state.tapCode}
style={styles.appContainer}>
{this.state.showResult && <ResultComponent/>}
</View>
);
}
}
}
export class LandingComponent extends React.Component {
getResult = (value) => {
this.state = {
tapCode: value.tapcode
}
console.log(this.state);
this.getScreen(this.state.tapCode);
}
getScreen = (tapcode) => {
setInterval(() => (
fetch('https://mywebservice.com/' + this.state.tapCode)
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson);
})
.then(this.state = {
showLanding: false,
showResult: true,
tapCode: tapcode,
resultResponse: this.responseJson
})
.catch((error) => {
console.error(error);
})
), 10000);
}
render() {
return (
<View style={styles.landingContainer}>
landing view, prompt for code
</View>
);
}
}
export class ResultComponent extends React.Component {
render() {
return (
<View style={styles.resultContainer}>
Show json output
</View>
);
}
}
There are plenty solutions, but you should definitely consider using a navigation library like react-navigation or react-native-router-flux to handle routing transitions between components.
My "dirty" suggestion would be: Let the App-Component render your Landing-Page and put the state property 'showResults' in there. Show the code-input if showResult is false, if not, show results.
export class LandingComponent extends React.Component {
constructor(props){
super(props);
this.state = {
showResults: false,
tapcode: null,
resultResponse
}
getResult = (value) => {
this.setState({tapCode: value.tapcode})
this.getScreen(this.state.tapCode);
}
getScreen = (tapcode) => {
setInterval(() => (
fetch('https://mywebservice.com/' + this.state.tapCode)
.then((response) => response.json())
.then((responseJson) => {
console.log(responseJson);
})
.then(function(res) {
this.setState({
showResult: true,
tapCode: tapcode,
resultResponse: res.json()})
return res;
})
.catch((error) => {
console.error(error);
})
), 10000);
}
render() {
return (
this.state.showResults ? <ResultComponent/> :
<View style={styles.landingContainer}>
call fetch here....
</View>
);
}
}
And please never mutate your state directly! Call setState() instead.
You need to move the API call up to App. Right now, showLanding is part of the App's state, but you're setting in the LandingComponent so you're not triggering a re-render.