I have a component that renders a list of credit cards. It receives a contract as a prop. A contract id can have multiple related credit cards. This list is fetched from the API via AJAX. The component can be visible or hidden on screen.
I fetch the list of credit cards via AJAX in state cards which is a tuple (key/value): [number, ICreditCardRecord[]]. That way I can keep track of which contract id the list I have in state belongs to.
So in componentDidUpdate I check if the component is not hidden and if the selected contract matches the one on which I saved the list. If not, I fetch the list again for the new contract.
Problem is, it triggers an endless loop. I read that it's not possible to setState inside componentDidUpdate as it triggers a re-render which triggers the function again which ... (endless loop).
So how can I do this? BTW I also use state to manage whether to show loading overlay or not (isLoading: boolean). I update this state prop when ever I start/stop loading.
import React, { Component } from 'react'
import { IApiVehicleContract, ICreditCardRecord } from '#omnicar/sam-types'
import * as api from 'api/api'
import { WithStyles, withStyles } from '#material-ui/core'
import CreditCard from 'components/customer/Contract/Details/CreditCard'
import NewCreditCard from 'components/customer/Contract/Details/NewCreditCard'
import AddCardDialog from 'components/customer/Contract/Details/AddCardDialog'
import { AppContext } from 'store/appContext'
import LoadingOverlay from 'components/LoadingOverlay'
import styles from './styles'
import alertify from 'utils/alertify'
import { t } from '#omnicar/sam-translate'
import { loadScript } from 'utils/script'
interface IProps extends WithStyles<typeof styles> {
contract: IApiVehicleContract | undefined
hidden: boolean
}
interface IState {
cards: [number, ICreditCardRecord[]]
showAddCreditCardDialog: boolean
isStripeLoaded: boolean
isLoading: boolean
}
class CustomerContractDetailsTabsCreditCards extends Component<IProps, IState> {
public state = {
cards: [0, []] as [number, ICreditCardRecord[]],
showAddCreditCardDialog: false,
isStripeLoaded: false,
isLoading: false,
}
public componentDidUpdate(prevProps: IProps, prevState: IState) {
const { cards } = this.state
const { contract, hidden } = this.props
// selected contract changed
const changed = contract && contract.serviceContractId !== cards[0]
// visible and changed
if (!hidden && changed) {
this.getCreditCards()
console.log('load')
}
}
public async componentDidMount() {
// load stripe
const isStripeLoaded = await loadScript('https://js.stripe.com/v3/', 'stripe-payment')
this.setState({ isStripeLoaded: !!isStripeLoaded })
}
public render() {
const { classes, contract, hidden } = this.props
const { cards, showAddCreditCardDialog, isLoading } = this.state
return (
<div className={`CustomerContractDetailsTabsCreditCards ${classes.root} ${hidden && classes.hidden}`}>
<React.Fragment>
<ul className={classes.cards}>
{cards[1].map(card => (
<CreditCard
className={classes.card}
key={card.cardId}
card={card}
onDeleteCard={this.handleDeleteCard}
onMakeCardDefault={this.handleMakeDefaultCard}
/>
))}
<NewCreditCard className={classes.card} onToggleAddCreditCardDialog={this.toggleAddCreditCardDialog} />
</ul>
<AppContext.Consumer>
{({ stripePublicKey }) => {
return (
<AddCardDialog
open={showAddCreditCardDialog}
onClose={this.toggleAddCreditCardDialog}
contract={contract!}
onAddCard={this.handleAddCard}
stripePublicKey={stripePublicKey}
isLoading={isLoading}
/>
)
}}
</AppContext.Consumer>
</React.Fragment>
<LoadingOverlay open={isLoading} />
</div>
)
}
private getCreditCards = async () => {
const { contract } = this.props
if (contract) {
// this.setState({ isLoading: true })
const res = await api.getContractCreditCards(contract.prettyIdentifier)
debugger
if (res) {
if (res.errorData) {
// this.setState({ isLoading: false })
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
console.error(res.errorData.message)
return
}
// sort list: put active first
const creditCards: ICreditCardRecord[] = res.data!.sort((a, b) => +b.isDefault - +a.isDefault)
const cards: [number, ICreditCardRecord[]] = [contract.serviceContractId, creditCards]
debugger
this.setState({ cards, isLoading: false })
}
}
}
private handleDeleteCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.deleteCreditCard(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// remove card from list
const creditCards = this.state.cards[1].filter(card => card.cardId !== cardId)
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card has been deleted'))
}
private handleMakeDefaultCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.makeCreditCardDefault(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// show new card as default
const creditCards = this.state.cards[1].map(card => {
const res = { ...card }
if (card.cardId !== cardId) {
res.isDefault = false
} else {
res.isDefault = true
}
return res
})
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card is now default'))
}
private toggleAddCreditCardDialog = () => {
this.setState({ showAddCreditCardDialog: !this.state.showAddCreditCardDialog })
}
private appendNewCardToList = (data: any) => {
const {cards: cardsList} = this.state
let creditCards = cardsList[1].map(card => {
return { ...card, isDefault: false }
})
creditCards = [data, ...creditCards]
const cards: [number, ICreditCardRecord[]] = [cardsList[0], creditCards]
this.setState({ cards })
// notify user
alertify.success(t('Credit card has been added'))
}
private handleAddCard = (stripe: stripe.Stripe) => {
// show spinner
this.setState({ isLoading: true })
const stripeScript = stripe as any
stripeScript.createToken().then(async (result: any) => {
if (result.error) {
// remove spinner
this.setState({ isLoading: false })
// notify user
alertify.warning(t('An error occurred... Please contact OmniCar if the problem persists. '))
return console.error(result.error)
}
// add credit card
const prettyId = this.props.contract ? this.props.contract.prettyIdentifier : ''
const req = await api.addCreditCard({ cardToken: result.token.id, isDefault: true }, prettyId)
if (req.errorData) {
alertify.warning(t('An error occurred while trying to add credit card'))
return console.error(req.errorData)
}
// remove spinner and close dialog
this.setState({ isLoading: false }, () => {
this.toggleAddCreditCardDialog()
this.appendNewCardToList(req.data)
})
})
}
}
export default withStyles(styles)(CustomerContractDetailsTabsCreditCards)
Related
I don't know why in another page, I use this way just different query and I can see data in view page source, but in this page , it not work. I wondering it cause I use localStorage value as params, i don't think problem come from query.
interface Props {
__typename?: 'ProductOfBill';
amount: number,
name: string,
totalPrice: number,
type: string,
unitPrice: number,
}
const Cart = () => {
const [products,setProducts] = useState<Props[]>([])
const { data } = useGetSomeProductQuery({
variables: { productList: productListForBill()},
notifyOnNetworkStatusChange: true
});
useEffect(() =>{
if(data?.getSomeProduct){
setProducts(data.getSomeProduct)
}
},[data])
return (
<>
...
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const apolloClient = initializeApollo();
await apolloClient.query<GetSomeProductQuery>({
query: GetSomeProductDocument,
variables: { productList: productListForBill() },
});
return addApolloState(apolloClient, {
props: {},
});
};
export default Cart;
I get localStorage value from this method.
export const productListForBill = () : GetProductForBill[] =>{
const returnEmtpyArray : GetProductForBill[] = []
if(typeof window !== "undefined"){
if(localStorage.getItem("products"))
{
const tempProduct = JSON.parse(localStorage.getItem("products") || "")
if(Array.isArray(tempProduct)){
return tempProduct
}
}
}
return returnEmtpyArray
}
and I custom Apollo Client like doc of Nextjs in github
import { useMemo } from 'react'
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from '#apollo/client'
import merge from 'deepmerge'
import isEqual from 'lodash/isEqual'
export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'
interface IApolloStateProps {
[APOLLO_STATE_PROP_NAME]?: NormalizedCacheObject
}
let apolloClient : ApolloClient<NormalizedCacheObject>
function createApolloClient() {
return new ApolloClient({
//type of "window"=== undifined
ssrMode: true,
link: new HttpLink({
uri: "http://localhost:4000/graphql",
credentials: "include",
}),
cache: new InMemoryCache()
)}
}
export function initializeApollo(initialState : NormalizedCacheObject | null = null) {
const _apolloClient = apolloClient ?? createApolloClient()
if (initialState) {
const existingCache = _apolloClient.extract()
cache
const data = merge(existingCache, initialState, {
arrayMerge: (destinationArray, sourceArray) => [
...sourceArray,
...destinationArray.filter((d) =>
sourceArray.every((s) => !isEqual(d, s))
),
],
})
_apolloClient.cache.restore(data)
}
if (typeof window === 'undefined') return _apolloClient
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
export function addApolloState(client : ApolloClient<NormalizedCacheObject>, pageProps: { props: IApolloStateProps }) {
if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract()
}
return pageProps
}
export function useApollo(pageProps : IApolloStateProps) {
const state = pageProps[APOLLO_STATE_PROP_NAME]
const store = useMemo(() => initializeApollo(state), [state])
return store
}
Answering
Cannot see data in view page source even though Cache of Apollo Client have data
These are client side methods, value will not be visible in view source but in evaluated source, look in the elements panel in chrome.
below is my code to update the state.customer slice. However when this runs, I see that the next state is not updated with the payload dispatched from the action. can some one please point out what I'm missing?
reducer:
const initialState1 = {"customer":{}};
const customer = (state = initialState1, action) => {
console.log('state:', state);
switch (action.type) {
case 'INIT_CUSTOMER_INFO':
return {
...state,
customer: action.payload
}
case 'UPD_CUSTOMER_INFO':
console.log(action);
return { ...customer, firstname: action.firstname }
default: return state;
}
}
export default customer;
action
export const initCustomer = customer => {
return {
type: 'INIT_CUSTOMER_INFO',
payload : customer
}
}
dispatch from LWC
import { LightningElement, track } from 'lwc';
import { connect } from 'c/connect';
import { updateCustomer, initCustomer } from 'c/actions';
import getJSONData from '#salesforce/apex/RGClass.getCartSummary';
const mapStateToProps = (state, ownProps) => ({
customer: state.customer
})
const mapDispatchToProps = (dispatch, ownProps) => ({
initCustomer : customer => dispatch(initCustomer(customer)),
updateCustomer : customer => dispatch(updateCustomer(customer))
})
export default class DigiForm extends LightningElement {
#track firstname;
showfirstname;
connectedCallback() {
//add the hook
connect(mapStateToProps, mapDispatchToProps)(this);//connects the
//api call
getJSONData()
.then(result => {
console.log('result:',result);
this.initCustomer(result);
})
.catch(error => {
console.log(error);
});
}
onContinue = () => {
let fn = this.template.querySelector('lightning-input').value;
console.log('firstname:', fn);
this.updateCustomer({ firstname : fn});
if(fn != null){
this.showfirstname = true;
}
}
}
Tried a couple of options but none worked. any help would be much appreciated!
this my updated version of intergrating redux and NextJS. Just to elobarate what I have done so far...
STEP 1. I've created a store.js file to set up my global store in reference to github's explanation from nextJS developers.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
import thunkMiddleware from 'redux-thunk';
import { customerListReducer } from './customerReducers';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const combinedReducer = combineReducers({
customerList: customerListReducer,
});
const reducer = (state, action) => {
console.log('Just Displaying the Store', state);
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
};
if (state.count) nextState.count = state.count; // preserve count value on client side navigation
return nextState;
} else {
return combinedReducer(state, action);
}
};
// create a makeStore function
const store = () =>
createStore(
reducer,
bindMiddleware([thunkMiddleware])
);
// export an assembled wrapper
export const wrapper = createWrapper(store);
STEP 2: Imported the wrapper above in my _app file to make the wrapper available across all pages in my application
import Nav from '../components/Nav';
import {wrapper} from '../reducers/store';
function MyApp({ Component, pageProps }) {
return (
<>
<Nav />
<Component {...pageProps} />
</>
);
}
export default wrapper.withRedux(MyApp);
STEP 3: CONFIGURATIONS
A) My Action that calls external API
import axios from 'axios';
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
export const listCustomers = () => async (dispatch) => {
try {
dispatch({
type: CUSTOMER_LIST_REQUEST,
});
const { data } = await axios.get(
'https://byronochara.tech/gassystem/api/v1/customers'
);
const result = data.results;
dispatch({
type: CUSTOMER_LIST_SUCCESS,
payload: result,
});
} catch (error) {
dispatch({
type: CUSTOMER_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
B)My Action Reducer
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
import { HYDRATE } from 'next-redux-wrapper';
export const customerListReducer = (state = { customers: [] }, action) => {
switch (action.type) {
case HYDRATE:
return { loading: true, customers: [] };
case CUSTOMER_LIST_REQUEST:
return { loading: true, customers: [] };
case CUSTOMER_LIST_SUCCESS:
return {
loading: false,
customers: action.payload,
};
case CUSTOMER_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
C)The finally bringing it all together in my index.js page to display the results:
import React, { useEffect } from 'react';
import Head from 'next/head';
import { useSelector} from 'react-redux';
import { listCustomers } from './../actions/customerActions';
import { wrapper } from '../reducers/store';
import styles from '../styles/Home.module.css';
const Home = () => {
//Select the loaded customers' list from central state
const customerList = useSelector((state) => {
console.log(state);
return state.customerList;
});
const { loading, error, customers } = customerList;
//displaying the customers data from the external API
console.log('Fetched Customers Data', customers);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{/* {loading && <h6>Loading...</h6>} */}
{/* {error && <h6>Error Occured...</h6>} */}
{/* {customers.map((customer) => (
<h3>{customer.customerName}</h3>
))} */}
{/* <ArticleList customers={customers} /> */}
</div>
);
};
// getStaticProp at build time
// getServerSideProp at every request slower
// getStaticPath to dynamically generate paths based on the data we are fetching
export const getStaticProps = wrapper.getServerSideProps(async ({ store }) => {
// console.log('STORE', store);
store.dispatch(listCustomers());
});
export default Home;
COMMENT ON THE PROBLEM I'M FACING FROM THE ABOVE CODE: once everything has been set up if you follow the code above, the code seems to run well the store is successfully created when I log the result on the console ``{ customerList: { loading: true, customers: [] } }. But then I guess this is the result from the HYDRATE action type since it will always be dispatch since am using getStaticProps``` that creates a new store instance in the server.
MAIN QUIZ: My challenge is how do I bypass the HYDRATED action and reconcile the server side state with the client side store and persist it and at least to finally be able to view the list from the external API. Thanks in advance. :)
I totally recommend you to use reduxjs/toolkit. It's very simple , less code, no wrappers, clean. And no matter your project on nextjs or created via CRA. Also you dont need to configure redux-thunk and redux-devtools cause they are enabled by default. Read documentation for more information ( how to persist state without any npm package and so on )
Here is a little example.
store.js
import { combineReducers, configureStore } from "#reduxjs/toolkit";
import userSlice from './user.slice.js';
//reducers
const rootReducer = combineReducers({
user: userSlice
});
const store = configureStore({
reducer: rootReducer,
});
export default store;
Wrap with Provider (in your case _app.js)
<Provider store={store}>
<Component {...pageProps} />
</Provider>
user.slice.js ( action + reducer )
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
const initialState = {
id: '',
email: '',
roles: []
};
// export async action
export const signIn = createAsyncThunk('user/signIn', async (data) => {
try {
const payload = await api.auth.signin(data).then((res) => res.data);
// do some stuff if you want
return payload ;
} catch (err) {
console.log(err.response);
}
});
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
removeUser(state, payload) {
//cant be an async method
return initialState;
},
extraReducers: (builder) => {
builder.addCase(signIn.fulfilled, (state, { payload }) => {
// payload from the async method above (asyncThunk)
return payload;
});
},
},
});
// export actions
export const { removeUser } = userSlice.actions;
// export reducer
export default userSlice.reducer;
Thats it. Last step to call actions from any component e.g.
import { useDispatch, useSelector } from 'react-redux';
import { signIn, removeUser } from '../actions/userSlice';
// in function component
// call hooks
const dispatch = useDispatch();
// read the store
const { user } = useSelector((state) => state);
// dispatch any action , example below
dispatch(signIn(userCredentials));
// or
dispatch(removeUser());
I has an Issue with setting Redux with NextJS and this is my final answer after some insight from mirik999 too.
A. my store.
import { configureStore } from '#reduxjs/toolkit';
//importing the slice file with sliced reducers
import customerListReducer from '../slices/customerSlice';
// const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));
const store = configureStore({
reducer: {
customerList: customerListReducer,
},
});
export default store;
B. The store is provided in my app component
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Nav />
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
C. The Slice file that automatically creates action creators and the reducer
import { createSlice } from '#reduxjs/toolkit';
//creating and action that calls API from a REST API backend
export const customersFetchedList = createAsyncThunk(
'customersList/customersListSuccess',
async () => {
try {
const { data } = await axios.get(
'https://example.com/api/your/endpoint'
);
const result = data.results;
//the payload
const payload = result;
return payload;
} catch (error) {
console.log(error.response);
const payload =
error.response && error.response.data.message
? error.response.data.message
: error.message;
return payload;
}
}
);
const initialState = {
loading: true,
customers: [],
error: false,
};
const customerListSlice = createSlice({
name: 'customersList',
initialState,
reducers: {
//reducer functions we've provided
customersRequest(state, action) {
if (state.loading == true) {
return state;
}
},
},
extraReducers: (builder) => {
initialState,
builder.addCase(customersFetchedList.fulfilled, (state, action) => {
state.loading = false;
state.customers = action.payload;
state.error = false;
return state;
});
},
});
export const {
customersRequest,
customersLoadingError,
} = customerListSlice.actions;
export default customerListSlice.reducer;
D. Then finally fired this action above in my component using the useEffect()
import React, { useEffect } from 'react';
import Head from 'next/head';
const Home = () => {
//method to fire the action
const dispatch = useDispatch();
//Select the loaded customers' list from central state
const customerList = useSelector((state) => state);
// const { loading, error, customers } = customerList;
useEffect(() => {
dispatch(listCustomers());
}, []);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{loading && <h6>Loading...</h6>}
{error && <h6>Error Occured...</h6>}
{customers.map((customer) => (
<h3>{customer.customerName}</h3>
))}
</div>
);
};
Thanks so much for your contribution. :)
I would like to add a progress indicator to Tesseract.js logging.
The example in docs works just fine, until setting a state hook into logger:
const worker = createWorker({
logger: (m) => {
setProgress(m) //new
}});
...
const [ocr, setOcr] = useState('Recognizing...');
const [progress, setProgress] = useState(null); //new
...
return (
<div className="App">
<p>
<LogComponent progress={progress}/> //new
</p>
</div>);
This causes the browser to slow down significantly (probably due React's way to re-render on each state update). Is there a way to get around this? Using React.memo perhaps?
You could design it into a hook so your entire component doesn't re-render. Here is a useTesseract hook you can use that I created: https://gist.github.com/KevinDanikowski/25cdcdda2ef4750bcf443f2027cc375a
Copy and Pasted:
import { useState, useEffect } from 'react'
import { createWorker } from 'tesseract.js'
export default function useTesseract({ tesseractLanguage = 'eng', log = false }) {
const [tesseractWorker, setTesseractWorker] = useState(null)
const [loadingModel, setLoadingModel] = useState(true)
const [modelError, setModelError] = useState(false)
const [imgResults, setImgResults] = useState({})
const [processing, setProcessing] = useState(false)
const [progress, setProgress] = useState(0)
const extractTextFromImage = (imageUrl) => {
const recognize = async () => {
const {
data: {
hocr: htmlOutput,
text,
// tsv, box, unlv
},
} = await tesseractWorker.recognize(imageUrl)
setProcessing(false)
setImgResults({ html: htmlOutput, text })
}
if (loadingModel) {
try {
setTimeout(recognize, 400)
} catch (e) {
console.error('Timeout Error:', e.message)
setImgResults({ error: true })
}
} else {
try {
setProcessing(true)
recognize()
} catch (e) {
console.error('Tesseract Error:', e.message)
setProcessing(false)
setImgResults({ error: true })
}
}
}
const logger = (m) => {
setProgress(m.progress)
if (log) {
console.info(m)
}
}
useEffect(() => {
const loadTesseract = async () => {
if (tesseractWorker) {
await tesseractWorker.loadLanguage(tesseractLanguage)
await tesseractWorker.initialize(tesseractLanguage)
console.info(`INFO: loaded ${tesseractLanguage} tesseract model`)
} else {
const tesseractWorker = createWorker({
logger,
// specify paths because sometimes the free CDN goes down
// corePath: '/static/tesseract-core.wasm.2.2.0.js',
// workerPath: '/static/tesseract-worker.v2.1.4.min.js',
})
setTesseractWorker(tesseractWorker)
await tesseractWorker.load()
await tesseractWorker.loadLanguage(tesseractLanguage)
await tesseractWorker.initialize(tesseractLanguage)
console.info(`INFO: loaded ${tesseractLanguage} tesseract model`)
setLoadingModel(false)
setModelError(true)
setLoadingModel(false)
}
}
loadTesseract().catch((e) => {
console.error(`ERROR: Failed to load tesseract model`, e.message)
setModelError(true)
setLoadingModel(false)
})
// TODO: Have to add a ref to reference the latest tesseractWorker in order to terminate
// return () => tesseractWorker.terminate()
}, [tesseractLanguage])
return {
imgResults,
loadingModel,
processing,
modelError,
progress,
extractTextFromImage,
}
}
I managed to implement and render the progress of the tesseract worker by putting my application render inside a class and using the setState method:
class App extends React.Component {
constructor(props){
super(props)
this.state = {
file: null
}
this.handleChange = this.handleChange.bind(this)
}
setProgress(m) {
if (m.progress !== 0 && m.progress !== 0.5 && m.progress !== 1){
var prog = "Progress: " + Math.round(m.progress*100) + "%"
this.setState({progress: prog})
}
}
worker = createWorker({
logger: m => this.setProgress(m),
});
doOCR = async () => {
await this.worker.load();
await this.worker.loadLanguage('eng');
await this.worker.initialize('eng');
const { data: { text } } = await this.worker.recognize(this.state.file);
this.setState({text: extractTotal(text),
progress: ""});
};
handleChange(event) {
this.setState({text: placeholder});
this.setState({
file: URL.createObjectURL(event.target.files[0]),
})
this.doOCR()
}
setText(input){
if (!input) {
return "Please select a receipt"
}
else {
return input
}
}
render() {
console.log("Text: " + this.state.text)
return (
<div className="container">
<p>{this.setText(this.state.text)}</p>
<p>{this.state.progress}</p>
<input type="file" onChange={this.handleChange}/>
<img src={this.state.file} className='logo' alt=""/>
</div>
);
}
}
I have this useSiren hook that should update its state with the incoming json argument but it doesnt.
On the first call the json is an empty object, because the fetch effect has not been run yet.
On the second call its also an empty object (triggered by loading getting set to true in App)
And on the third call its filled with valid data. However, the valid data is not applied. The state keeps its initial value.
I guess somehow setSiren must be called to update it, since initial state can only be set once. But how would I do that? Who should call `setSiren?
import { h, render } from 'https://unpkg.com/preact#latest?module';
import { useEffect, useState, useCallback } from 'https://unpkg.com/preact#latest/hooks/dist/hooks.module.js?module';
import htm from "https://unpkg.com/htm#latest/dist/htm.module.js?module";
const html = htm.bind(h);
function useFetch({
method = "GET",
autoFetch = true,
href,
body
}) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState()
const [response, setResponse] = useState()
const [isCancelled, cancel] = useState()
const [json, setJson] = useState({})
const sendRequest = async payload => {
try {
setLoading(true)
setError(undefined)
const response = await fetch(href.replace("http://", "https://"), {
method
})
const json = await response.json()
if (!isCancelled) {
setJson(json)
setResponse(response)
}
return json
} catch (err) {
if (!isCancelled) {
setError(err)
}
throw err
} finally {
setLoading(false)
}
}
if (autoFetch) {
useEffect(() => {
sendRequest(body)
return () => cancel(true)
}, [])
}
return [{
loading,
response,
error,
json
},
sendRequest
]
}
function useSiren(json) {
const [{ entities = [], actions = [], links, title }, setSiren] = useState(json)
const state = (entities.find(entity => entity.class === "state")) || {}
return [
{
title,
state,
actions
},
setSiren
]
}
function Action(props) {
const [{ loading, error, json }, sendRequest] = useFetch({ autoFetch: false, href: props.href, method: props.method })
const requestAndUpdate = () => {
sendRequest().then(props.onRefresh)
}
return (
html`
<button disabled=${loading} onClick=${requestAndUpdate}>
${props.title}
</button>
`
)
}
function App() {
const [{ loading, json }, sendRequest] = useFetch({ href: "https://restlr.io/toggle/0" })
const [{ state, actions }, setSiren] = useSiren(json)
return (
html`<div>
<div>State: ${loading ? "Loading..." : (state.properties && state.properties.value)}</div>
${actions.map(action => html`<${Action} href=${action.href} title=${action.title || action.name} method=${action.method} onRefresh=${setSiren}/>`)}
<button disabled=${loading} onClick=${sendRequest}>
REFRESH
</button>
</div>
`
);
}
render(html`<${App}/>`, document.body)
Maybe what you want to do is to update the siren state when the json param changes? You can use a useEffect to automatically update it.
function useSiren(json) {
const [{ entities = [], actions = [], links, title }, setSiren] = useState(json)
useEffect(() => { // here
setSiren(json)
}, [json])
const state = (entities.find(entity => entity.class === "state")) || {}
return [
{
title,
state,
actions
},
setSiren
]
}
The pattern mentioned by #awmleer is packaged in use-selector:
import { useSelectorValue } from 'use-selector';
const { entities=[], actions=[], title} = json;
const siren = useSelectorValue(() => ({
state: entities.find(entity => entity.class === 'state') || {},
actions,
title
}), [json]);
Disclosure I'm author and maintainer of use-selector