How to prevent re-rendering when switching the pages in SolidJS? - frontend

I'm struggling with the re-rendering issue in the SolidJS application. I have two routes, Home and Detail. A user can explore items in Home, and click the link on the item name to switch a page to Detail to check out detailed information.
export default function Home() {
const [items, setItems] = createSignal<Item[]>([]);
onMount(async () => {
setItems(
await fetchItemsThroughExpensiveAPI()
);
});
return (
<main>
<For each={items()}>
{(item) => (
<A href={`/item/${item.id}`}>{item.name}</A>
)}
</For>
</main>
);
}
export default function Detail() {
const params = useParams<{ id: string }>();
return (
<main>
// Some detailed information for the item ...
</main>
);
}
At this point, the API(fetchItemsThroughExpensiveAPI) will be called back when the user returns to the Home from Detail. I'm expecting this it is caused by re-rendering. How do I prevent re-rendering Home whenever a user returns to Home from another page to avoid unnecessary API calls?

Use a resource to fetch the data outside the Home component. If you need to fetch the data once during application's life, cache it.
https://www.solidjs.com/docs/latest/api#createresource
Lets make it more clear. There are different patterns to render async data, data that resides in a remote location.
Fetch as you render: In this pattern, data is fetched when the component mounts.
ComponentA below uses this pattern. Whenever it is re-rendered, data will be re-fetched.
Fetch then render: In this pattern, data is fetched outside the component, in one of its parent's scope. When component mounts it can use whatever is currently available, by whatever, I mean the request may not be resolved yet so state will be pending.
Resource API is build to make use of this pattern.
ComponentB below uses this pattern. Since data is fetched outside the component, re-rendering has no effect on it.
import { Accessor, Component, createSignal, Match, Switch } from 'solid-js';
import { render } from 'solid-js/web';
interface State { status: 'pending' | 'resolved' | 'rejected', data?: any, error?: any };
function getData(): Accessor<State> {
const [state, setState] = createSignal<State>({ status: 'pending' });
setTimeout(() => {
setState({ status: 'resolved', data: { name: 'John Doe', age: 30 } });
}, 1000);
return state;
};
const ComponentA = () => {
const state = getData();
return (
<Switch fallback={<div>Not Found</div>}>
<Match when={state().status === 'pending'}>
Loading...
</Match>
<Match when={state().status === 'resolved'}>
{JSON.stringify(state().data)}
</Match>
<Match when={state().status === 'rejected'}>
{JSON.stringify(state().error)}
</Match>
</Switch>
);
};
const ComponentB: Component<{ state: Accessor<State> }> = (props) => {
return (
<Switch fallback={<div>Not Found</div>}>
<Match when={props.state().status === 'pending'}>
Loading...
</Match>
<Match when={props.state().status === 'resolved'}>
{JSON.stringify(props.state().data)}
</Match>
<Match when={props.state().status === 'rejected'}>
{JSON.stringify(props.state().error)}
</Match>
</Switch>
);
};
const App = () => {
const state = getData();
const [show, setShow] = createSignal(false);
const handleClick = () => setShow(prev => !prev);
return (
<div>
{show() && (<ComponentA />)}
{show() && (<ComponentB state={state} />)}
<div><button onclick={handleClick}>Toggle Show Components</button></div>
</div>
)
};
render(() => <App />, document.body);
Here you can see it in action: https://playground.solidjs.com/anonymous/32518df5-9840-48ea-bc03-87f26fecc0f4
Here we simulated an async request using setTimeout. This is very crude implementation to prove a point. If you are going to fetch a remote resource, you should use the Resource API which provides several utilities like automatic re-fecthing when request parameters change.
There are few issues with your implementation. First and foremost, async data in never guaranteed to be received, so you should handle failures.
Since it takes some time to receive the remote data, you have to show the user some indicator of the ongoing request, like a loader.
If API call is an expensive operation, you have to cache the result, rather than forcing the UI not to re-render. In this sense, your approach is very problematic.

Utilize the useEffect hook. When the component is first loaded, the useEffect hook can be used to retrieve the items from the API and store them in a state variable. This way, when the component re-renders, it can access the stored items in the state variable instead of making a new API call.
export default function Home() {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
async function fetchData() {
setItems(await fetchItemsThroughExpensiveAPI());
}
fetchData();
}, []);
return (
<main>
<For each={items}>
{(item) => (
<A href={`/item/${item.id}`}>{item.name}</A>
)}
</For>
</main>
);
}

Related

Load WooCommerce data on demand and show it in a DataGrid by Syncfusion

I need to load all the products in my nodeJS application with WooCommerce Rest Api. I use the WooCommerce REST API - JavaScript Library and the Syncfusion Grid Component. Because I can't load all data at once, I wanted to use the Load data on demand like this, but I can't find any documentation or examples on this.
I have something like this:
import React from 'react';
import { useEffect, useState } from "react";
import { GridComponent, ColumnsDirective, ColumnDirective, Resize, Sort, ContextMenu, Filter, Page, ExcelExport, PdfExport, Edit, Inject } from '#syncfusion/ej2-react-grids';
import WooCommerceRestApi from "#woocommerce/woocommerce-rest-api";
var WooCommerce = new WooCommerceAPI({
url: 'http://example.com',
consumerKey: 'ck_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
consumerSecret: 'cs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
wpAPI: true,
version: 'wc/v1'
});
const WooCommerceProducts = () => {
const [products, setProducts] = useState([]);
useEffect(() => {
fetchOrders();
}, []);
let fetchOrders = () => {
WooCommerce
.get("products", {
per_page: 100,
page: 1
})
.then((response) => {
if (response.status === 200) {
setProducts(response.data);
}
})
.catch((error) => { });
};
return (
<div className='m-2 md:m-10 p-2 md:p-10 bg-white rounded-3xl'>
<Header category="Page" title="WooCommerce Orders" />
<GridComponent
id='gridcomp'
dataSource={orders}
allowPaging
allowSorting>
<ColumnsDirective>
<ColumnDirective field='id' />
<ColumnDirective field='name' />
<ColumnDirective field='slug' />
<ColumnDirective field='status' />
...
</ColumnsDirective>
<Inject services={[Resize, Sort, ContextMenu, Filter, Page, ExcelExport, PdfExport]} />
</GridComponent>
</div>
)
}
export default WooCommerceProducts
Please help and thx
If you are using any custom services, I suggest you use the custom-binding feature to bind the data to the grid. I would like to share the behavior of custom-binding in EJ2 Grid. 
For every grid action (such as Filter, Page, etc.,), I have triggered the dataStateChange event, and, in the event arguments, I have sent the corresponding action details (like skip, take, filter field, value, sort direction, etc.,) Based on that, you can perform the action in your service, return the data as a result, and count objects. 
Note: ‘dataStateChange’ event is not triggered at the Grid initial render. If you are using a remote service, you need to call your remote service by manually with a pagination query (need to set the skip value as 0 and take a value based on your pageSize of pageSettings in Grid. If you are not defined pageSize in pageSettings, you need to send the default value 12 ) in load event of Grid. Please return the result like as "{result: […], count: …}" format to Grid. 
‘dataSourceChanged’ event is triggered when performing CRUD actions in Grid. You can perform the CRUD action in your service using action details from this event, and, you need to call the endEdit method to indicate the completion of the save operation. 
Custom-binding: https://ej2.syncfusion.com/react/documentation/grid/data-binding/data-binding/#custom-binding
Demo: https://ej2.syncfusion.com/react/demos/#/material/grid/custom-binding
sample: https://stackblitz.com/edit/react-v64sms-wx3hsy?file=index.js

How should I fetch payment intent secret for Stripe Elements in my Next.js app?

I am trying to implement Stripe payments in my Next.js app as described in the guide here: https://stripe.com/docs/payments/quickstart
The guide tells me that in order to use Stripe Elements for my checkout form, I need to know payment intent. It says:
Create PaymentIntent as soon as the page loads
The issue is - our website will not have a separate payments page, the payment form will be displayed inside the modal, which is loaded on every page of the website. That means, I would have to fetch the payment intent for any user who ever visits any page on our website, whether they're planning to purchase the course or not, just so that I could display the payment form inside the modal. That doesn't seem right to me.
Can you give me some advice, let me know if there's a better way to handle this?
Another issue is that this guide tells me that I should pass the fetched payment intent clientSecret as an option to <Elements/> wrapper.
And if I hover on <Elements/> wrapper in my VSCdoe, it tells me:
[...] Render an Elements provider at the root of your React app so that it is available everywhere you need it. [...]
So, does that mean I have to put <Elements/> wrapper into my _app.tsx file? And that means I'd have to fetch the payment intent clientSecret inside of the _app.tsx? So that my app would fetch payment intent secret any time any user ever loads any page on my website?
Again, this seems pretty weird, wouldn't it slow things down, add extra requests and loading time to all my pages, and create a whole bunch of payment intents that are never used?
Render the payment form in a modal in Layout.js and wrap the
entire project in the Layout component
place this code in _app.js
import React, { useEffect, useState } from "react"
import { loadStripe } from "#stripe/stripe-js"
import { Elements } from "#stripe/react-stripe-js"
import Layout from "../components/Layout"
import PaymentModalForm from "../components/PaymentModalForm"
const promise = loadStripe("pk_test_....")
// replace pk_test_... with your publishable key
const API_URL = "http://localhost:8000"
// replace API_URL with your backend server url
const App = ({ Component, pageProps }) => {
const [secret, setSecret] = useState(null)
useEffect(() => {
const fetchSecret = async () => {
const response = await fetch(`${API_URL}/create_intent`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: [{ id: 'adidas boost', quantity: 2}]
})
})
const { client_secret } = await response.json()
setSecret(clientSecret)
}
fetchSecret()
}, [])
const options = {
clientSecret: secret,
appearance: { theme: "stripe"}
}
return (
{secret && (
<Elements stripe={promise} options={options}>
<Layout>
<Component {...pageProps} />
</Layout>
</Elements>
)}
)
}
export default App
Then in your Layout.js, fill in this code
import PaymentModalForm from "../components/PaymentModalForm"
import React, { useEffect, useState } from "react"
const Layout = ({ children }) => {
const [showModal, setShowModal] = useState(false)
const handleClick = () => {
if (showModal) {
setShowModal(false)
} else {
setShowModal(true)
}
}
return (
<div>
<div className="container">
{children}
<button onClick={handleClick}>Show Payment Modal</button>
</div>
{showModal ? (
<div className="modal fade">
<div className="modal-dialog">
<div className="modal-content">
<PaymentModalForm />
</div>
</div>
</div>
) : ( null )}
</div>
)
}
export default Layout
There's more work to be done in PaymentModalForm.js

tiptap ReactNodeViewRenderer how to render the original view

I'm using tiptap and trying to extend the Paragraph node to wrap some extra stuff around its view. I used <NodeViewWrapper> and <NodeViewContent> as the guides said.
const ParagraphWrapper = () => {
return (
<NodeViewWrapper>
<NodeViewContent />
</NodeViewWrapper>
)
}
const ParagraphExt = Paragraph.extend({
addNodeView() {
return ReactNodeViewRenderer(ParagraphWrapper)
}
})
export default function App() {
const editor = useEditor({
extensions: [
Document,
Text,
ParagraphExt, // <<<< text-align was not rendered
// Paragraph, // <<<< This worked
TextAlign.configure({
types: ["paragraph"]
}),
],
content: `<p style="text-align: center">This is a paragraph</p>`,
})
return (
<>
<EditorContent editor={editor} />
<pre>{JSON.stringify(editor?.getJSON?.(), null, 2)}</pre>
</>
);
}
However, this seems to render the node from scratch. Thus, other extensions, such as textAlign no longer works.
I only need to wrap a thin layer around whatever was rendered originally. How do I do that?
Code Sandbox
You still get access to the attrs being passed to the node which is available in props. You can use that info to style your rendered content as you wish.
const ParagraphWrapper = (props) => {
const textAlign = props.node.attrs.textAlign;
return (
<NodeViewWrapper>
<NodeViewContent style={{textAlign}} />
</NodeViewWrapper>
);
};

Which one is better for pagination, fetchMore or getServerSideProps?

I want to crate a shopping online website.It have product pagination fuction.
In the docs of Nextjs, they recommend use getStaticProps and getStaticPaths than other(fetchMore in getStaticProps), but when I search Nextjs pagination, almost docs or tutorial use getServerSideProps or getInitialProps.
i tried to use with fetchMore and i don't see any problem,so why they use getServerSideProps?
My code
let paginationOptions: PaginationOptionsInput = {
skip: 0,
type: "SALES_DESC",
};
const Index = () => {
const [filterChecked,setFilterChecked] = useState("SALES_DESC")
const [currentPage,setCurrentPage] = useState(1)
const {data,fetchMore,loading} = useGetProductsQuery({
variables:{
paginationOptions
},
notifyOnNetworkStatusChange: true
})
console.log(data?.getProducts.products)
const handlePageChange = (page:number) =>{
setCurrentPage(page)
paginationOptions.skip = 4 * (page-1)
fetchMore({
variables:paginationOptions
})
}
return (
<div>
<Navbar />
<div className="distance">
...
{data?.getProducts.products &&
data.getProducts.products!.map((product) => (
<div className="col l-3 m-4 c-6" key={product.id}>
<div className={styles.productItem}>
<img src={product.thumbnail} />
<h2>{product.productName}</h2>
<h3>{product.priceToDisplay} VND</h3>
<div className={styles.paidInfo}>
<h4>{product.sales}</h4>
<h4>{product.commentAmount}</h4>
</div>
</div>
</div>
))}
<Pagination
className="pagination-bar"
currentPage={currentPage}
totalCount={data?.getProducts.totalCount!}
pageSize={data?.getProducts.pageSize!}
onPageChange={(page : number) => handlePageChange(page)}
/>
...
</div>
<Footer />
</div>
);
};
export const getStaticProps: GetStaticProps = async () => {
const { data,error,loading } = await client.query<GetProductsQuery>({
query: GetProductsDocument,
variables: { paginationOptions },
notifyOnNetworkStatusChange: true
});
return {
props: {
paginationProducts: data.getProducts,
},
};
};
export default Index;
Maybe I am missing something, but:
getInitialProps - to forget in nextjs v11+
getServerSideProps - every time page is called, [server-side]
getStaticProps and getStaticPaths - runs during build, [server-side]
I recommend you to take a look at this library [client-side]
When to use getServerSideProps(1) vs getStaticProps (2)
When you have pages where data needs to be loaded every time (example: settings, clients, etc).
When you have a lot of pages that need to be rendered, to reach huge performance. (example: product pages, some text pages - for example, "terms and conditions" with data from DB) where you know links previously. It's why in each getStaticProps page we need getStaticPaths.
SEO optimized pages are getStaticProps
So, if you have some rules for the pages, for example /product/1 /product/2 ... /product/n you need to create page with getStaticProps
Also, take a look at this

React update component after loading data

So I have a component that shows categories from firestore, the component shows nothing the first time but when I click navbar button again it does show the data stored in firestore.
Here is the component file :
import * as React from "react";
import Category from "./Category";
import connect from "react-redux/es/connect/connect";
import {getCategories} from "../reducers/actions/categoryAction";
class CategoriesList extends React.Component{
constructor(props) {
super(props);
this.state = ({
categoriesList: [{}]
})
}
componentWillMount() {
this.props.getCategories();
this.setState({categoriesList: this.props.categories});
this.forceUpdate();
}
render() {
return (
<div className={'container categories'}>
<div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
<div className={'col s24 m12'}>
<p>Create New Category</p>
</div>
</div>
<div className={'row'}>
<div className={'col s24 m12'}>
{/*{() => this.renderCategories()}*/}
{this.state.categoriesList && this.state.categoriesList.map(category => {
return <Category category={category} key={category.id}/>
})}
</div>
</div>
</div>
);
}
}
const mapDisptachToProps = (dispatch) => {
return {
getCategories: () => dispatch(getCategories()),
}
};
const mapStateToProps = (state) => {
return {
categories: state.category.categories
}
};
export default connect(mapStateToProps, mapDisptachToProps)(CategoriesList)
And here is the reducer file:
import db from '../firebaseConfig'
const initState = {
categories: []
};
const categoryReducer = (state=initState, action) => {
switch (action.type) {
case 'CREATE_CATEGORY':
db.collection("Categories").add({
category: action.category.name
})
.then(function(docRef) {
db.collection("Categories").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// console.log(`${doc.id} => ${doc.data().category}`);
if(doc.id === docRef.id) {
state.categories.push({id: doc.id, name: doc.data().category});
console.log(state.categories)
}
});
});
})
.catch(function(error) {
console.error("Error adding document: ", error);
});
break;
case 'GET_CATEGORIES':
console.log('Getting data from firestore');
db.collection("Categories").get().then((querySnapshot) => {
if(state.categories.length !== querySnapshot.size) {
querySnapshot.forEach((doc) => {
state.categories.push({id: doc.id, name: doc.data().category});
});
}
});
break;
}
return state;
};
export default categoryReducer
Is there any way to update the component after fully loading the data? or a way to load all the data in the initalState?
There are few things one needs to understand. First, this.props.getCategories() performs an action that is asynchronous in nature and hence in the very next line this.setState({categoriesList: this.props.categories});, we wont get the required data.
Second, Storing props to state without any modification is un-necessary and leads to complications. So try to use the props directly without storing it. In case you are modifying the obtained props, make sure you override getDerivedStateFromProps apropiately.
Third, Try to use componentDidMount to perform such async operations than componentWillMount. Refer when to use componentWillMount instead of componentDidMount.
Fourth(important in your case), Reducer should not contain async operations. Reducer should be a synchronous operation which will return a new state. In your case, you need to fetch the data elsewhere and then dispatch within your db.collection(..).then callback. You can also use redux-thunk, if you are using too many async operations to get your redux updated.
So #Mis94 answer should work if you follow the fourth point of returning the new state in the reducer rather than mutating the redux directly in the db().then callback
First, you don't need to store the component's props in the state object. This is actually considered an anti-pattern in react. Instead of doing this, just use your props directly in your render method:
render() {
return (
<div className={'container categories'}>
<div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
<div className={'col s24 m12'}>
<p>Create New Category</p>
</div>
</div>
<div className={'row'}>
<div className={'col s24 m12'}>
{/*{() => this.renderCategories()}*/}
{this.props.categories && this.props.categories.map(category => {
return <Category category={category} key={category.id}/>
})}
</div>
</div>
</div>
);
}
Hence in your componentWillMount you only need to initiate your request:
componentWillMount() {
this.props.getCategories();
}
You can also do it in componentDidMount() lifecycle method.
Now when your request resolves and your categories update in the store (Redux) they will be passed again to your component causing it to update. This will also happen with every update in the categories stored in the store.
Also you don't have to call forceUpdate like this unless you have components implementing shouldComponentUpdate lifecycle method and you want them to ignore it and do a force update. You can Read about all these lifecycle methods (and you have to if you are using React) here.

Resources