The question comes from a issue where I need data binding and save it to a reducer so I can use pusher to modify the data when needed and it changes in real-time. The problems I find are that:
I am new in react and I don't really know much about how to bind data variables.
I am using remote data with fetch so the data can be refreshed but it can't seem to find a way to properly bind or even save it to a reducer.
Below, the relevant code:
class MainTable extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<MaterialTable
tableRef={this.tableRef}
columns={columnsSetup}
options={materialTableOptions}
data={query =>
new Promise((resolve, reject) => {
pageQuery = query.page;
pageSizeQuery = query.pageSize;
let url = GET_ORDERS_URL;
url += 'qtt=' + pageSizeQuery;
url += '&page=' + pageQuery;
fetch(url)
.then(response => response.json())
.then(result => {
resolve({
data: result.data,
page: result.page,
totalCount: result.totalElems
});
});
})
}
/>
);
}
}
Data fetching should be done in one of the react lifecycle methods. These are build-in functions that will be called on specific "life" events of your component, for example when it gets mounted (componentDidMount).
You should read the docs thoroughly to really get the hang of it.
To give you an example of the implementation of a lifecycle method, I fixed your code below.
A couple of important, but also opinionated, subjects to look into are: lifecycle methods, state, Async/await (instead of promises), Components: classes and hooks.
class MainTable extends React.Component {
state = {
data: {}
};
componentDidMount() {
this.fetchData();
}
fetchData = async query => {
let url = `${GET_ORDERS_URL}qtt=${query.pageSize}&page=${query.page}`;
const response = await fetch(url);
const result = response.json();
this.setState({
data: {
data: result.data,
page: result.page,
totalCount: result.totalElems
}
});
};
render() {
return (
<MaterialTable
tableRef={this.tableRef}
columns={columnsSetup}
options={materialTableOptions}
data={this.state.data}
/>
);
}
}
Below is a functional component (exactly the same logic as above) that uses hooks:
import React, { useEffect, useState } from 'react';
function MainTable() {
const [data, setData] = useState({});
useEffect(() => {
const fetchData = async query => {
let url = `${GET_ORDERS_URL}qtt=${query.pageSize}&page=${query.page}`;
const response = await fetch(url);
const result = response.json();
setData({
data: result.data,
page: result.page,
totalCount: result.totalElems
});
};
fetchData();
}, [data]);
return (
<MaterialTable
columns={columnsSetup}
options={materialTableOptions}
data={data}
/>
);
}
Material-table is a fantastic out the box soloution for tables however, I don't particularity like the way the pagination is handled. I use await and async operators rather than promises and wanted my component to be responsible for managing the state rather than these promise methods that material-table wants you to provide. Material-table provide a way to completely override their pagination logic so this is what I did.
material-table's Pagination component appears to just be a wrapper Material UI's TablePagination component.
const [data,setData] = React.useState([])
const [pagination, setPagination] = React.useState({take: 20, page: 0})
const [count, setCount] = React.useState(0)
React.useEffect(() => {
async function fetch() {
var {result, count} = await someAsyncCallToServer()
setData(fetch)
setCount(count)
}
fetch()
}
,[pagination])
const handleChangePage = (event, newPage) => {
setPagination(old => ({...old, page: newPage}))
}
...
<MaterialTable
data={data}
...
components = {{
Pagination : props =>
<TablePagination
rowsPerPage={pagination.take}
onChangeRowsPerPage={take => setPagination(old=> ({...old, take}))}
page={pagination.page}
count={count}
onChangePage={handleChangePage}
/>
}}
/>
Related
I have a two Api's which gets some source and another to add sources.
While displaying the source(images ..) on the screen, in the mean time I am adding some new sources. Since componentDidMount runs only at the start, I can not force componentDidMount to run again whenever a new source added
Here the part of the code:
App.tsx:
export class App extends React.Component<MyProps, MyState> {
constructor(props: any) {
super(props)
this.state = {
sources: [],
currentImage: -1,
}
}
async getSources() {
let allData = await axios.get('/playlist')
return allData.data.data // getting allData from the Api
}
async componentDidMount() {
const sources = await this.getSources()
this.setState(
{
sources: sources,
currentImage: 0,
}
)
}
render() {
<div>
<Playlist onChange={() => this.getSources()} />
.
.
.
</div>
}
}
Playlist.tsx
export function Playlist(props: {onChange: () => void}) {
const {register, handleSubmit} = useForm()
const [data, setData] = useState('')
const onSubmit = async (data: any) => {
console.log('data', await props.onChange())
const url = '/add'
try {
await axios.post(url, data)
await props.onChange(). // trying to call the function after adding new source
} catch (error) {
console.log('Error')
}
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
...
</form>
)
}
As seen in the code, I try to call the function inside the componentDidMount again(after new source is added) to update sources but seems it doesn't work.
I am trying to use the search api of Open Library.
Usually, if you are going to use a functional component, you will do it like this on your api file:
import axios from 'axios';
export default axios.create({
baseuRL: 'http://openlibrary.org/search.json'
})
And then you will import this on a file where you will fetch the data:
import booksAPI from '../apis/books';
const books = () => {
useEffect(() => {
books()
}, [])
const books = async() => {
const res = await booksAPI.get('?author=tolkien');
console.log(res.data);
}
}
This is expected to console.log the data on your terminal. However, using the class component with axios and componentDidMount to fetch the data.
class BookList extends Component {
constructor(props) {
super(props)
this.state = {
books: []
}
}
componentDidMount(){
const booksResponse = async() => {
const response = await booksAPI.get('?author=tolkien');
console.log(response.data)
}
}
This is complaining about the await keyword and doesn't console.log the data. Also, I am not sure how I can convert the useEffect to a class component so it can perform side effects?
const BookList = () => {
// State variable where you can store the data
const [books, setBooks] = useState([]);
// Effect, which would be called on component mount because of empty array of dependencies -> [] (look for useEffect lifecycles) and set the data to state variable. After this, component will re-render.
useEffect(() => {
const fetchBooks = async () => {
const response = await booksAPI.get('?author=tolkien');
setBooks(response.data);
console.log(response.data);
}
fetchBooks();
}, []);
return ...
}
Also, make sure that you're adding query/mutation/subscription to your GraphQL document right before the name of query/mutation/subscription, as someones told you in the comment.
In case you're looking for class component realization, your code should look like this:
class BookList extends Component {
constructor(props) {
super(props)
this.state = {
books: []
}
}
fetchBooks = () => {
const response = booksAPI.get('?author=tolkien');
this.setState({ books: response.data });
}
componentDidMount(){
fetchBooks();
}
...
}
Here I have created a simple example in React for you.
componentDidMount() {
this.booksResponse();
}
booksResponse = async () => {
const response = await axios.get(
'https://openlibrary.org/search.json?author=token'
);
console.log(response.data);
};
Here is Stackblitz link.
https://stackblitz.com/edit/react-zk8nhq
If you find any confusing then Please comment here I can create more example for you.
Edit: final solution at the bottom.
I am trying to build a simple web-app to display some data stored in firestore database, using React Table v7. I'm pretty new to React and javascript in general so forgive me if I use the wrong terminology.
At first I had put the function to fetch data inside App.jsx, passing it to state with setState within a useEffect hook, and it was working without an issue.
Then a colleague suggested that it is a good practice passing data to a component state instead of the app state, and that's where problems started to arise.
As of now, I cannot manage to populate the table. The header gets rendered but there's nothing else, and the only way I can make it show data is to make a small change in Table.jsx while npm start is running (such as adding or changing the output of any console.log) and saving the file. Only then, data is displayed.
I've been trying everything I could think of for about 2 days now (last thing I tried was wrapping Table.jsx into another component, but nothing changed).
I tried console.loging all the steps where data is involved to try and debug this, but I'm failing to understand where the problem is. Here's the output when the app loads first:
My code currently:
utility function to fetch data from Firestore
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
},
);
console.log('func output', dataArray);
return dataArray;
};
export default parseData;
Table.jsx
import { useTable } from 'react-table';
const Table = ({ columns, data }) => {
const tableInstance = useTable({ columns, data });
console.log('table component data received', data);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
} = tableInstance;
return (
// html boilerplate from https://react-table.tanstack.com/docs/quick-start#applying-the-table-instance-to-markup
);
};
export default Table;
TableContainer.jsx
import { useState, useEffect, useMemo } from 'react';
import parseData from '../utils';
import Table from './Table';
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
const getData = async () => {
const dataFromServer = await parseData(db);
setData(dataFromServer);
console.log('container-useEffect', dataFromServer);
};
getData();
}, [db]);
const columns = useMemo(() => [
{
Header: 'ID',
accessor: 'id',
},
// etc...
], []);
console.log('container data', data);
return (
<>
<Table columns={columns} data={data} />
</>
);
};
export default TableContainer;
App.jsx
import firebase from 'firebase/app';
import 'firebase/firestore';
import TableContainer from './components/TableContainer';
import Navbar from './components/Navbar';
// ############ INIT FIRESTORE DB
const firestoreCreds = {
apiKey: process.env.REACT_APP_API_KEY,
authDomain: process.env.REACT_APP_AUTH_DOMAIN,
projectId: process.env.REACT_APP_PROJECT_ID,
};
if (!firebase.apps.length) {
firebase.initializeApp(firestoreCreds);
}
const db = firebase.firestore();
function App() {
return (
<div>
<Navbar title="This is madness" />
<div>
<TableContainer db={db} />
</div>
</div>
);
}
export default App;
Edit: in the end, using the suggestions below, this is the final solution I could come up with. I was annoyed by having to wrap the API call in an async func within useEffect, so this is what I did.
utility function to fetch data from Firestore
const parseData = (db) => {
const snapshot = db.collection('collection_name').get();
return snapshot.then(
(querySnapshot) => {
const dataArray = [];
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
return dataArray;
},
);
};
export default parseData;
TableContainer.jsx
Here I also added the flag didCancel within useEffect to avoid race conditions, according to this and this it seems to be a best practice.
// imports
const TableContainer = ({ db }) => {
const [data, setData] = useState([]);
useEffect(() => {
let didCancel = false;
parseData(db)
.then((dataFromServer) => (!didCancel && setData(dataFromServer)));
return () => { didCancel = true; }
}, [db]);
// ...
In parseData function this line return dataArray; is execute before the snapshot is resolved. You need to change parseData and return a Promise and resolve when data is ready:
const parseData = async (db) => {
const dataArray = [];
const snapshot = db.collection('collection-name').get();
return new Promise(resolve => {
snapshot.then(
(querySnapshot) => {
querySnapshot.forEach((doc) => {
const document = { ...doc.data(), id: doc.id };
dataArray.push(document);
});
resolve(dataArray); //--> resolve when data is ready
},
);
})
};
In your TableContainer, you initialize data with an empty array. That's being sent along to Table until you finish getting data from your server. If you don't want that to happen, you should change your default (in useState) to something like false and explicitly handle that case (e.g. display "Please wait. Loading" if data === false).
The asynchronicity in the parseData function is plain wrong (and the implementation a smidge too complex...). Reimplement it something like this...
const parseData = async (db) => {
// Note `await` here.
const snapshot = await db.collection('collection-name').get();
return snapshot.map((doc) => ({ ...doc.data(), id: doc.id }));
};
export default parseData;
I'm building a small ToDo list app with React Apollo and GraphQL. In order to add a new ToDo item I click "Add" button that redirects me to a different URL that has a form. On form submit I perform a mutation and update the cache using update function. The cache gets updated successfully but as soon as I return to the main page with ToDo list, the component triggers an http request to get the ToDo list from the server. How do I avoid that additional request and make ToDoList component pull data from the cache ?
My AddToDo component:
const AddToDo = () => {
const { inputValue, handleInputChange } = useFormInput();
const history = useHistory();
const [addToDo] = useMutation(ADD_TODO);
const onFormSubmit = (e) => {
e.preventDefault();
addToDo({
variables: { title: inputValue },
update: (cache, { data: { addToDo } }) => {
const data = cache.readQuery({ query: GET_TODO_LIST });
cache.writeQuery({
query: GET_TODO_LIST,
data: {
todos: [...data.todos, addTodo],
},
});
history.push("/");
},
});
};
return (
...
);
};
And ToDoList component
const ToDoList = () => {
const { data, loading, error } = useQuery(GET_TODO_LIST);
if (loading) return <div>Loading...</div>;
if (error || !loading) return <p>ERROR</p>;
return (
...
);
};
Works as expected.
Why unecessary? Another page, new component, fresh useQuery hook ... default(?) fetchPolicy "cache-and-network" will use cached data (if exists) to render (quickly, at once) but also will make request to be sure current data used.
You can force "cache-only" but it can fail if no data in cache, it won't make a request.
How do I convert this class component to a functional component?
What I am trying to achieve is to subscribe and unsubscribe from firebase using useEffect()
class PostsProvider extends Component {
state = { posts: [] }
unsubscribeFromFirestore = null;
componentDidMount = () => {
this.unsubscribeFromFirestore = firestore
.collection('posts')
.onSnapshot(snapshot => {
const posts = snapshot.docs.map(collectIdAndDocs);
this.setState({ posts });
});
}
componentWillUnmount = () => {
this.unsubscribeFromFirestore();
}
This is how I'd convert your component. You'd useState() to create your posts state and then a useEffect is pretty straightforward to move. The main thing you'd want to make sure of is that your dependency array is correct for it so it doesn't subscribe and unsubscribe too often (or not often enough).
function PostsProvider(){
const [posts,setPosts] = useState([]);
useEffect(() => {
const unsubscribeFromFirestore = firestore
.collection('posts')
.onSnapshot(snapshot => {
const posts = snapshot.docs.map(collectIdAndDocs);
setPosts(posts);
});
return () => {
unsubscribeFromFirestore();
}
}, [])
}