How to customize data provider request call? - reactjs

I'm new to react and react-admin.
I'm, using jsonServerProvider (in my App.js I have the following):
import jsonServerProvider from 'ra-data-json-server';
I'd like to create a custom bulk action. In a list, select many items and click a button to "connect" them. I tried to use UPDATE_MANY, but this calls my endpoint multiple times, so it's not suitable. Ideally I need the request to call my endpoint like so: url.to.myendpoint?ids=1,2,3 or even better pass an array of IDs in the body and use a PUT request.
Just to understand how things work and debug network calls, I tried also the GET_MANY, in the dataproviders page, the request seems to get the IDs like so: { ids: {mixed[]}, data: {Object} }
But the request is sent to the server like so: url.to.myendpoint?id=1&id=2&id=3 which in my python/flask backend is not nice to parse.
I've spent a bunch of time reading the docs, e.g.:
https://github.com/marmelab/react-admin/blob/master/docs/Actions.md
https://react-admin.com/docs/en/actions.html
https://marmelab.com/react-admin/Actions.html
I tried different approaches and I could not achieve what I want. So again please help me to make my custom bulk button work.
My bulk button is called ConnectItemsButton and the code looks like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Button, crudUpdateMany } from 'react-admin';
import { showNotification, GET_MANY } from 'react-admin';
import dataProvider from './dataProvider';
class ConnectItemsButton extends Component {
handleClick = () => {
const { selectedIds } = this.props;
dataProvider(GET_MANY, 'items/connect', { ids: selectedIds })
.then(() => {
showNotification('Connected!');
})
.catch((e) => {
showNotification('Error.', 'warning')
});
};
render() {
return (
<Button label="Associate" onClick={this.handleClick} />
);
}
}
export default connect(undefined, { crudUpdateMany })(ConnectItemsButton);
Note that the contents of ./dataProvider (it's the same provider used in the App.js file and passed to the <Admin> in the props):
import jsonServerProvider from 'ra-data-json-server';
export default jsonServerProvider('http://127.0.0.1:5000/api');
In my list I created it, the button is displayed properly, so here I share the code snippet:
const PostBulkActionButtons = props => (
<Fragment>
<ConnectItemsButton {...props} />
</Fragment>
);
...
export const ItemsList = props => (
<List {...props} bulkActionButtons={<PostBulkActionButtons />}>
...
In my backend endpoint items/connect I simply need to get a comma separated list of IDs to parse, that's it.
A simple working solution would be awesome, or at least point me in the right direction. Thanks for your help.

The way I would do this is by using react-admin's dataActions. Your action would be something like this:
crudCreate('items/connect', { selectedIds: selectedIds }, basePath , redirectUrl)
I recommend using a custom dataProvider (e.g. if you use jsonDataProvider, in your App.js import where you see ra-data-json-server: if you use WebStorm Ctrl + click on it and copy the code e.g. to customJsonDataProvider.js and fix eventual warnings, e.g. import lines should be moved at the top) and pass it as props to your Admin component. In your customJsonDataProvider you will have a convertDataRequestToHTTP, or something similar, which manages the CRUD actions and returns the url and HTTP method that you want to use.
An example of what you want to do would be:
const convertDataRequestToHTTP = (type, resource, params) => {
let url = '';
const options = {};
switch (type) {
...
case CREATE: {
...
if (type === 'items/connect') {
const { data: { selectedIds } } = params;
url = `${apiUrl}?${selectedIds.reduce((acc, id) => `${acc};${id}`)}`
options.method = 'GET';
}
...
break;
}
...
}
return { url, options };
}
In your dataProvider.js, modify the import to use the custom provider you created, e.g.:
import jsonServerProvider from './customJsonServer';
The code in your AssociateTradesButton.js should work fine as is.
You can find the documentation for creating you own dataProvider here.

I think using Promise.all will solve the issue, see this link for reference https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/src/index.js#L140

Related

How to use the useQueries hook in react-query v4

So I'm perfectly able to retrieve matches from a competition with the Football-data.org API and display them in my react/typescript application using the useQuery hook from react-query:
import {CompetitionProps} from "../App";
import {getMatchesFromApi, Match} from "../api/GetMatchesFromApi";
import {List, ListItem} from "#mui/material";
import {useQuery} from "#tanstack/react-query";
function MatchesList({competitions}: CompetitionProps) {
const { isLoading, error, data, isFetching} = useQuery(["matches"], async () => {
return await getMatchesFromApi(competitions);
});
if (isLoading || isFetching) {
return (<div>Loading</div>);
} else {
return (
<List>
{data?.map((match: Match) => {
return (
<ListItem key={match.id}>{match.homeTeam.shortName} - {match.awayTeam.shortName}</ListItem>
);
})}
</List>
);
}
}
export default MatchesList;
However I want all matches from a list of competitions (the competitions can be different based on user preferences). When reading the docs of react-query, the useQueries hook should do the trick. These docs unfortunately don't show how to handle the results of the useQueries hook:
https://tanstack.com/query/v4/docs/reference/useQueries
I tried to use it like this:
import {
useQueries,
UseQueryOptions,
UseQueryResult
} from "#tanstack/react-query";
import { getMatchesFromApi, Match } from "./GetMatchesFromApi";
const allCompetitions = [2003, 2021];
function MatchesList() {
const results = useQueries({
queries: allCompetitions.map<UseQueryOptions<Match[]>>(
(competition: number) => {
return {
queryKey: ["competition", competition],
queryFn: async () => await getMatchesFromApi(competition)
};
}
)
});
return <div>{results.length}</div>;
}
export default MatchesList;
Even though I'm not even attempting to display the data yet, just using this code that only prints the length of the results array, will cause the code to fetch every few seconds. This will result in 429 (too many requests) responses of the football-data.org API fairly quickly.
This behavior doesn't match at all with the default staleTime and cacheTime settings explained in:
https://medium.com/doctolib/react-query-cachetime-vs-staletime-ec74defc483e
The question: How do I stop this infinite fetching loop? The reason I wanted to use react-query at all is to lazy fetch the match data only once.
Full project to reproduce this problem:
https://codesandbox.io/s/serene-raman-47n2uz
(If you want to reproduce it, you'll have to register on football-data.org for free and generate an API key. Put your key in the Key.ts file. I don't want to put mine on the internet).
Thanks!

Understanding Context in Svelte (convert from React Context)

I have a react app that uses ContextAPI to manage authentication and I am trying to implement a similar thing in Svelte. [Web Dev Simplified][1]
In Authenticate.js I have this:
import React, { useContext, useState, useEffect } from "react"
import { auth } from "../firebase"
const AuthCt = React.createContext()
export function Auth() {
return useContext(AuthCt)
}
export function AuthComp({ children }) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password)
}
function logout() {
return auth.signOut()
}
useEffect(() => {
const unmount = auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
})
return unmount
}, [])
const value = {
currentUser,
login,
signup
}
return (
<AuthCt.Provider value={value}>
{!loading && children}
</AuthCt.Provider>
)
}
This context is used in other Login.js component like this:
import { Auth } from "./Authenticate"
const Login = () => {
const { currentUser, login } = Auth()
And in App.js I have:
import { AuthComp } from "./Authenticate";
function App() {
return (
<AuthComp>
<div> All others go here </div>
</AuthComp>
);
}
How do I achieve this in Svelte, particularly the Authenticate context?
I haven't been able to do much in Svelte as I don't know how to proceed from here. So far I have AuthComp.svelte. I don't know if I am doing the right thing.
<script>
import { getContext, setContext } from 'svelte';
import { auth } from '../firebase';
import { writable } from 'svelte/store';
let Auth = getContext('AuthCt')
setContext('Auth', Auth)
let currentUser;
let loading = true;
const unmount = auth.onAuthStateChanged(user => {
currentUser = user;
loading = false
});
function login(email, password) {
return auth.signInWithEmailandPassWord(email,password)
}
function logout() {
return auth.signOut()
}
const value = { currentUser, login, signUp }
</script>
<slot value={value}></slot>
Migrating from React Context to Svelte
Context in Svelte and React may seem similar, but they are actually used differently. Because at the core, Svelte's context is much more limited. But that's ok. In fact, it actually will make your code simpler to write and understand.
In Svelte, you have more tools at your disposal for passing data round your app (and keeping it in sync) than just context. Each one does pretty much one thing (making everything predictable), and they do it well. Of these, you have:
Context
Stores
Props
As someone who's recently switched from React to Svelte, I think I can help explain some of the differences between each of these and help you avoid some of my conceptual mistakes. I'll also go over some differences in life cycle methods, because if you used to use useEffect, you might feel very lost since Svelte doesn't have an equivalent API. Yet combining everything together in Svelte will make everything simple.
Context
Context in Svelte does one thing: pass data from a parent component to any children (not necessarily direct children). Unlike in React, context is not reactive. It is set once when the component mounts, and then will not be updated again. We'll get to "reactive context" in a second.
<!-- parent.svelte -->
<script>
import { setContext } from 'svelte'
setContext('myContext', true)
</script>
<!-- child.svelte -->
<script>
import { getContext } from 'svelte'
const myContext = getContext('myContext')
</script>
Notice that context involves two things, a key and a value. Context is set to a specific key, then the value can be retrieved using that key. Unlike React, you do not need to export functions to retrieve the context. Both the key and value for the context can be anything. If you can save it to a variable, you can set it to context. You can even use an object as a key!
Stores
If you have data that needs to stay in sync in multiple places across your app, stores are the way to go. Stores are reactive, meaning they can be updated after they're created. Unlike context in either React or Svelte, stores don't simply pass data to their children. Any part of your app can create a store, and any part of your app can read the store. You can even create stores outside of Svelte components in separate JavaScript files.
// mystore.ts
import { writable } from 'svelte/store'
// 0 is the initial value
const writableStore = writable(0)
// set the new value to 1
writableStore.set(1)
// use `update` to set a new value based on the previous value
writableStore.update((oldValue) => oldValue + 1)
export { writableStore }
Then inside a component, you can subscribe to the store.
<script>
import { writableStore } from './mystore'
</script>
{$writableStore}
The dollar sign subscribes to the store. Now, whenever the store is updated, the component will rerender automatically.
Using stores with context
Now that we have stores and context, we can create "reactive context"(a term I just made up, but it works). Stores are great because they're reactive, and context is great to pass data down to the children components. But we can actually pass a store down through context. This makes the context reactive and the store scoped.
<!-- parent.svelte -->
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
const writableStore = writable(0)
setContext('myContext', writableStore)
</script>
<!-- child.svelte -->
<script>
import { getContext } from 'svelte'
const myContext = getContext('myContext')
</script>
{$myContext}
Now, whenever the store updates in the parent, the child will also update. Stores can of course do much more than this, but if you were looking to replicate React context, this is the closest you can get in Svelte. It's also a lot less boilerplate!
Using "reactive context" with "useEffect"
Svelte does not have an equivalent of useEffect. Instead, Svelte has reactive statements. There's a lot on these in the docs/tutorial, so I'll keep this brief.
// doubled will always be twice of single. If single updates, doubled will run again.
$: doubled = single * 2
// equivalent to this
let single = 0
const [doubled, setDoubled] = useState(single * 2)
useEffect(() => {
setDoubled(single * 2)
}, [single])
Svelte is smart enough to figure out the dependencies and only run each reactive statement as needed. And if you create a dependency cycle, the compiler will yell at you.
This means that you can use reactive statements to update stores (and hence update the context). Here, the valueStore will be update on every keystroke to the input. Since this store is passed down through context, any child can then get the current value of the input.
<script>
import { setContext } from 'svelte'
import { writable } from 'svelte/store'
// this value is bound to the input's value. When the user types, this variable will always update
let value
const valueStore = writable(value)
setContext('inputContext', valueStore)
$: valueStore.set(value)
</script>
<input type='text' bind:value />
Props
For the most part, props function exactly the same in React and Svelte. There are a few differences because Svelte props can take advantage of two-way binding (not necessary, but possible). That's really a different conversation though, and the tutorial is really good at teaching two-way binding with props.
Authentication in Svelte
Ok, now after all of that, let's look at how you'd create an authentication wrapper component.
Create an auth store
Pass the auth store down via context
Use Firebase's onAuthStateChanged to listen to changes in auth state
Subscribe to the auth store in the child
Unsubscribe from onAuthStateChanged when the parent is destroyed to prevent memory leaks
<!-- parent.svelte -->
<script>
import { writable } from 'svelte/store'
import { onDestroy, setContext } from 'svelte'
import { auth } from '../firebase'
const userStore = writable(null)
const firebaseUnsubscribe = auth.onAuthStateChanged((user) => {
userStore.set(user)
})
const login = (email, password) => auth.signInWithEmailandPassWord(email,password)
const logout = () => auth.signOut()
setContext('authContext', { user: userStore, login, logout })
onDestroy(() => firebaseUnsubscribe())
</script>
<slot />
<!-- child.svelte -->
<script>
import { getContext } from 'svelte'
const { login, logout, user } = getContext('authContext')
</script>
{$user?.displayName}
In Svelte, context is set with setContext(key, value) in a parent component, and children can access the value object with getContext(key). See the docs for more info.
In your case, the context would be used like this:
<script>
import { getContext, setContext } from 'svelte';
import { auth } from '../firebase';
import { writable } from 'svelte/store';
// you can initialize this to something else if you want
let currentUser = writable(null)
let loading = true
// maybe you're looking for `onMount` or `onDestroy`?
const unmount = auth.onAuthStateChanged(user => {
currentUser.set(user)
loading = false
});
function login(email, password) {
return auth.signInWithEmailandPassWord(email,password)
}
function logout() {
return auth.signOut()
}
const value = { currentUser, login, signUp }
setContext('Auth', value)
</script>
{#if !loading}
<slot></slot>
{/if}
Here, currentUser, login, and signup (not sure where that's coming from?) are set as context with setContext(). To use this context, you would probably have something like this:
<!-- App -->
<AuthComp>
<!-- Some content here -->
<Component />
</AuthComp>
<!-- Component.svelte -->
<script>
import { getContext } from 'svelte'
const { currentUser, login, signup } = getContext('Auth')
// you can subscribe to currentUser with $currentUser
</script>
<div>some content</div>
As written in the docs, context is not reactive, so currentUser is first converted into a store so it can be subscribed to in a child. As for the useEffect, Svelte has lifecycle functions that you can use to run code at different points, such as onMount or onDestroy.
If you're new to Svelte, their tutorial is very comprehensive with plenty of examples that you can refer back to.
Hope this helped!

Gatsby Replace Static Query Data at Runtime

I'm new to Gatsby & React and I'm trying to figure out how to get the best of both worlds of prerendering and dynamic data.
The query alone works great for getting the data at build time and passing it as a prop to the Menu component where each menu item is rendered. However, at run time, I would like to pull the data again from the DB and have it update the data, for example, if there was a price change, etc.
I know I could just rebuild the whole project but I would like to have this as a fallback.
How can I make the query send the data to the Menu component and then also [send the data again?] when the DB call is done.
Code that is currently not working as expected:
index.jsx
import React, { useEffect } from "react"
import Layout from "../components/layout"
import SEO from "../components/seo"
import Menu from '../components/menu'
import { graphql } from "gatsby"
import firebase from "gatsby-plugin-firebase"
const IndexPage = (props) => {
useEffect(() => {
// use this hook to make db call and re-render menu component with most up to date data
var db = firebase.firestore();
let docs = []
db.collection(`public/menu/${process.env.restaurantId}`).get().then(val => {
val.forEach(doc => {
docs.push({ node: doc.data() })
});
console.log('docs', docs)
props.data.allMenuItem.edges = docs; // i have no idea what i'm doing
})
}, [])
return (
<Layout>
<SEO title="Home" />
<Menu menuItems={props.data.allMenuItem.edges}></Menu>
</Layout>
)
}
// use this query for prerendering menu items
export const query = graphql`
query MyQuery {
allMenuItem {
edges {
node {
available
name
group
}
}
}
}
`;
export default IndexPage
You aren't supposed to modify React properties; any value that can change should be part of the state of the component. See Can I update a component's props in React.js?
However, the following code ought to do it. Create a state and give it the property value as default value. Then update it after the data loads on the client side.
const IndexPage = props => {
const [menuItems, setMenuItems] = useState(props.data.allMenuItem.edges.map(({node}) => node))
useEffect(() => {
// use this hook to make db call and re-render menu component with most up to date data
let db = firebase.firestore()
db.collection(`public/menu/${process.env.restaurantId}`)
.get()
.then(setMenuItems)
}, [])
return (
<Layout>
<SEO title="Home" />
<Menu menuItems={menuItems}></Menu>
</Layout>
)
}
Note that I've switched to using the data format you get from firestore (without node) rather than the one from Gatsby, so you'd need to modify your Menu component to not expect an extra level of nesting (with node) if you use this code.

Fetching Data from API using NextJS and Material UI React

I am trying to create dynamic pages that shows individual book details (i.e. title/author etc) on a separate page based on a query string of the "id" of each book. However, I am having difficulty in understanding how to make a request to a API endpoint using NextJS that will get the book details based on its "id". I would like to use Material UI as a UI Framework.
ISSUE: When I run npm run dev the book page loads but the book's "props" are not being passed along to the BookAttributes component. The console.log(book) I added in the book page is undefined and the console.log(title) in BookAttributes is undefined as well.
I've tested the API endpoint in POSTMAN and it appears to work.
When I refactor the same code using Semantic UI-React instead of Material UI, the book pages load correctly.
I am using the NextJS Material UI starter template from the Material UI website as a baseline.
I am fairly new to NextJS and Material UI so your assistance and guidance would be greatly appreciated. Thank you for your help on this!
Here is the code I have so. I have tried to keep in clean and simple.
BOOK PAGE (within 'pages' directory)
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
function Book({ book }) {
console.log(book)
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
Book.getInitalProps = async ({ query: { _id } }) => {
const url = 'http://localhost:3000/api/book';
const payload = { params: { _id }}
const response = await axios.get(url, payload)
return { book: response.data }
}
export default Book;
BOOK API ENDPOINT (within 'pages/api' directory)
import Book from '../../models/Book';
import connectDb from '../../utils/connectDb';
connectDb()
export default async (req, res) => {
const { _id } = req.query
const book = await Book.findOne({ _id })
res.status(200).json(book);
}
BOOK ATTRIBUTE COMPONENT (within 'components' directory)
import React from 'react';
function BookAttributes({ title }) {
console.log(title)
return (
<>
<h1>{title}</h1>
</>
)
}
export default BookAttributes;
You should be using dynamic routes here if you want to work with data-fetching methods like getStaticProps or getServerSideProps.
You can create a page like pages/book/[id].js. But to generate the page you have to decide what data-fetching method you want to run. If the data for the page doesn't change very often you can choose to use static-site-generation using getStaticProps which will generate the pages at build time. If the data will be changing a lot you can either do server-side-rendering using getServerSideProps or fetch the data client-side.
Here is an example for your use-case that you can use for server-side-rendering using getServerSideProps, keep in mind the API call inside getServerSideProps might fail so you should have appropriate error handling.
In pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getServerSideProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
Using static-site-generation
Because the page is dynamic you have to provide a list of paths for which nextjs will generate the pages. You can do that by exporting an async function called getStaticPaths.
in pages/book/[id].js
import axios from 'axios';
import BookAttributes from '../components/Book/BookAttributes';
export const getStaticPaths = async () => {
// here you have two options if you know all the ids of the books
// you can fetch that data from the api and use all the ids to generate
// a list of paths or show a fallback version of page if you don't know all
// ids and still want the page to be static
// Pseudo code might look like this
const res = await axios.get('api-endpoint-to-fetch-all-the-books')
const paths = res.data.map(book => ({ params: { id: book.id }}))
return {
paths,
fallback: false
}
}
export const getStaticProps = async (ctx) => {
const bookId = ctx.params?.id
const url = 'http://localhost:3000/api/book';
const response = await axios.get(url, { params: { _id: bookId} })
return {
props: {
book: response.data
}
}
}
function Book({ book }) {
return (
<>
<h1>Book Page</h1>
<BookAttributes {...book} />
</>
)
}
export default Book;
The fallback property in the returned value of getStaticPaths is somewhat important to understand. If you know all the necessary id for the pages you can set the fallback to false. In this case nextjs will simply show a 404 error page for all the paths that were not returned from the function getStaticPaths.
If fallback is set to true nextjs will show a fallback version of page instead of a 404 page for the paths that were not returned from the getStaticPaths function. Now where should you set fallback to true? Let's suppose in your case new books are added to the database frequently, but the data for the books doesn't change very often so you want the pages to be static. In this case, you can set fallback to true and generate a list of paths based on avaliable book ids. For the new books nextjs will first show the fallback version of the page than fetch the data based on the id provided in the request and will send the data as JSON which will be used to render the page in the client.

How should I organize my React Dispatch URLs

The app I'm working on is using react-router-redux. Currently the way I handle link-clicking is I'll have the following:
clickEvent = (e) => {
e.preventDefault()
this.props.dispatch(push(`/products/${product.id}`))
}
repeating itself in many different components. Same way applied to other links as well.
Recently we have decided to make changes to the url structure and suddenly I find myself searching for all dispatch(push()) and fixing them one by one. So now I'm trying to find a way to store these urls in one single place.
The obvious and easy way I tried was by having a routing_utils.js:
import React from "react"
import { push } from "react-router-redux"
// Store all the possible urls here
export function visitHome(dispatch) {
dispatch(push('/'))
}
export function visitProductShow(dispatch, productId) {
dispatch(push(`/products/${productId}`))
}
export function visitProductReviews(dispatch, productId) {
dispatch(push(`/products/${productId}`/reviews))
}
and then using it like so:
import { visitProductShow } from "../../routing_utils"
// ... inside a component:
clickEvent = (e) => {
const { dispatch, product } = this.props
visitProductShow(dispatch, product.id)
}
or you know, just store the urls without passing in dispatch, then call them like so: dispatch(push(productShowLink(id))), either way it doesn't feel right.
Can this be handled through redux actions? If so, how can I go about it? I am looking for a better approach to handle this issue, thank you for your time.

Resources