Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useSWR to fetch only one time #450

Closed
oran1248 opened this issue Jun 10, 2020 · 19 comments
Closed

useSWR to fetch only one time #450

oran1248 opened this issue Jun 10, 2020 · 19 comments

Comments

@oran1248
Copy link

How can I use swr to fetch data only once? I don't need to refresh the data and revalidate.
I prefer to use swr library for this and not just sending simple request and showing data, for couple of reasons:

  1. swr manages cache
  2. swr prevent a situation of update after unmount (no need to cancel request)
    and more great things swr has to offer..

So how can I use swr for this? What I need to put in the options param?

Is it a good idea to use it for this purpose? It feels like I'm using a sledgehammer to crack a nut.

Thanks!

@nfantone
Copy link

@oran1248 If you are planning on fetching data strictly once, then why would you care about cache at all?

At any event, you could turn off all refreshing and revalidation using:

useSWR(key, {
  revalidateOnFocus: false,
  revalidateOnMount:false,
  revalidateOnReconnect: false,
  refreshWhenOffline: false,
  refreshWhenHidden: false,
  refreshInterval: 0
});

Note that some of the above are actually the default values.

@oran1248
Copy link
Author

@oran1248 If you are planning on fetching data strictly once, then why would you care about cache at all?

At any event, you could turn off all refreshing and revalidation using:

useSWR(key, {
  revalidateOnFocus: false,
  revalidateOnMount:false,
  revalidateOnReconnect: false,
  refreshWhenOffline: false,
  refreshWhenHidden: false,
  refreshInterval: 0
});

Note that some of the above are actually the default values.

Because swr can show cached data (if fetched before) until revalidation finishes. Am I missing something here?

@nfantone
Copy link

@oran1248 You said you want to "fetch data only once" - but then talk about the benefits of "revalidation" and "cache". Those two concepts imply, and are strictly tied to, fetching multiple times.

I may have been thrown off the scent here. I'm sorry if that's the case.

@oran1248
Copy link
Author

@nfantone What I mean is "fetch data only once per page load". if the user loads the page again, it will use the data in the cache and revalidate. Maybe the correct term is "revalidate only once".

@nfantone
Copy link

nfantone commented Jun 10, 2020

@oran1248 Depending on what you mean by "load the page again", I don't think swr would work the way you seem to be describing it. swr doesn't store or cache data in a persistent storage out-of-the-box. If you reload your browser, you'll always fetch the same thing again, effectively losing any cache or revalidation capabilities. Both features come into play when data changes after the first paint and needs to be re-fetched in order to update your view.

Once again, apologies if I'm being misleading here and not really following your question.

@oran1248
Copy link
Author

@nfantone You are right about page reloading, but what about router navigations?
I'm using react+next.js. I think that in that case the data remains in the cache.

@sergiodxa
Copy link
Contributor

From what I understand, what you want is to fetch data the first time you need it and then never fetch it again? You can achieve that with the options @nfantone give you, this way SWR will not fetch the data if it is already in the cache, you can then use mutate to manually trigger a revalidation if you need it.

This data will be kept in the in-memory cache so page navigation using Next.js or React Router or any other Client-Side navigation will keep the data in cache avoiding further requests.

I also think you have a misunderstanding in what revalidation is, revalidation means you already have the data and you are fetching it again to ensure is not stale, what you want is to avoid automatic revalidation to happen if you already have the data, the initial fetch is not a revalidation.

Only exception of the last part is when you provide initialData to SWR and disable revalidateOnMount, then SWR will not fetch the data.

@oran1248
Copy link
Author

@sergiodxa loud and clear! thanks!

@chiqui3d
Copy link

chiqui3d commented Jul 1, 2020

Hello,
I'm sorry but I don't quite understand. I was currently using useSWR to get the data to a form.When I edit that form and change an image, that image disappears when you do the revalidation, because it brings me back the same data.

I guess and I understand that it has to do with the options mentioned above by the user @nfantone:

useSWR(key, {
  revalidateOnFocus: false,
  revalidateOnMount:false,
  revalidateOnReconnect: false,
  refreshWhenOffline: false,
  refreshWhenHidden: false,
  refreshInterval: 0
});

So what should I do? Disable all of the above options. Or use natively without your library as in this example.

const [question, setQuestion] = useState(defaultValues);
    const [mounted, setMounted] = useState(false);
    const [error, setError] = useState(false);

    useEffect(() => {
        async function initialData() {
            const response = await fetch(config.apiNamespace + crud.apiRouter+'/'+props.id, {
                method: 'GET'
            })
            let data = await response.json();
            if (response.status !== 200){
                setError(data)
            }else{
                setQuestion(data)
            }
            setMounted(true);
        }
        initialData().then()
    },[])

    if (!mounted) {
        return (<Loader/>)
    }
    if (error) {
        return (<ErrorPage statusCode={error.data.status} title={error.message} statusText={error.message}/>)
    }

Any clarification?

@nfantone
Copy link

nfantone commented Jul 1, 2020

@chiqui3d A couple of pointers here.

First, if this is initial data that you are fetching, you might wanna give initialData a try.

function MyComponent({ initialData }) {
  const { data } = useSWR('/api/data', fetcher, { initialData });
  // ...
}

If you are using an SSR solution, like next.js, you could return initialData from getServerSideProps.

Another thing I can think of is that it seems like you are binding the values of the object you fetch with useSWR directly to inputs in a form - that's why, I assume, you are actually seeing this behaviour in which fields revert back to its original value on revalidation. Instead, you might want to try and separate the initial values and the edited values in different states (if you are not using it already, formik may help with that). Or use plain uncontrolled inputs.

{initialData && (
  <form>
    <input name="name" type="text" defaultValue={initialData.name} />
    <input name="age" type="number" defaultValue={initialData.age} />
  </form>
}

@chiqui3d
Copy link

chiqui3d commented Jul 1, 2020

Hello @nfantone,

Thank you very much now using defaultValue instead of value, it works perfectly.

Sorry for my rookie question, so is this what you recommend to use? Defaultvalue instead of value.

This is my complete component using useSWR without Next.js in this case, but I usually use Next.js.

import React, {useEffect, useState} from "react";
import {Breadcrumb, Breadcrumbs} from '@src/Layout/Breadcrumbs'
import FormBootstrap from "react-bootstrap/Form"
import TopActions from "@src/Form/TopActions";
import ResponseSuccessForm from "@src/Form/ResponseSuccessForm";
import ResponseFailForm from "@src/Form/ResponseFailForm";
import BottomActions from "@src/Form/BottomActions";
import {FetchWithResponse} from "@src/helpers/fetchWithResponse";
import ColWithInput from "@src/Form/ColWithInput";
import Row from "@src/Form/Row";
import Answer from "@src/Pages/questions/partials/Answer";
import config from "@root/config";
import Loader from "@src/Layout/Loader";
import ErrorPage from "@src/Layout/ErrorPage";
import useSWR from "swr";
import fetchSWR from "@src/helpers/fetchSWR";

const defaultValues = {
    title: '',
    answers: [
        {
            title: '',
            image: '',
            order: '',
            question_id: ''
        },
        {
            title: '',
            image: '',
            order: '',
            question_id: ''
        },
        {
            title: '',
            image: '',
            order: '',
            question_id: ''
        }
    ],
}

const crud = {
    name: 'question',
    pageName: 'Editar pregunta',
    listName: 'Preguntas',
    listRouter: 'questions/index',
    actionApiRouter: 'questions',
    submitButton: 'Actualizar pregunta',
    submitAction: 'PUT',
    actionMessage: 'Pregunta actualizada correctamente'
}

const QuestionsEdit = (props) => {
    const [validated, setValidated] = useState(false)
    const [responseForm, setResponseForm] = useState('')
    const [responseFailForm, setResponseFailForm] = useState(null)
    const [mounted, setMounted] = useState(false);
    const [question, setQuestion] = useState(defaultValues);
    const {data, error} = useSWR(config.apiNamespace + crud.actionApiRouter+'/'+props.id, url => fetchSWR(url, {
        method: 'GET',
    }, false));

    useEffect(() => {
        async function initialData() {
            setQuestion(data)
            setMounted(true)
        }
        initialData().then()
    },[data])

    if (!mounted) {
        return (<Loader/>)
    }
    if (mounted && error) {
        const statusText = error.statusText || error.toString()
        return (<ErrorPage statusCode={error.status} title={statusText} statusText={statusText}/>)
    }

    const onChangeHandler = (event, index) => {
        const element = event.target;
        setQuestion(prevState => {
            return {...prevState, [element.name]: element.value};
        });
    }

    const handleSubmit = async (event) => {
        const form = event.currentTarget
        if (form.checkValidity() === false) {
            event.preventDefault()
            event.stopPropagation()
        }
        const imageErrors = [];
        const ImageError = ({index}) => <div className="d-block">Por favor agrega la imagen para la respuesta {index}</div>
        question.answers.forEach((answer,index) => {
            if (answer['image'] === ''){
                imageErrors.push(<ImageError key={"error-"+index} index={index+1}/>);
            }
        })
        if (imageErrors.length > 0 ){
            event.preventDefault()
            event.stopPropagation()
            setValidated(false);
            setResponseFailForm(imageErrors);
            return;
        }

        setValidated(true);
        if (form.checkValidity() === true) {
            event.preventDefault()
            setResponseFailForm('')
            setResponseForm('')
            const successCallback = async (res, newData) => {
                setValidated(false)
                setResponseForm(crud.actionMessage);
            };
            const errorCallback = async (res, data) => {
                setValidated(false)
                setResponseFailForm('Error ' + data.status + '. ' + data.message)
            };

            return await FetchWithResponse(
                config.apiNamespace + crud.actionApiRouter+'/'+question.id,
                props,
                JSON.stringify(question),
                crud.submitAction,
                true,
                successCallback,
                errorCallback
            );

        }
    }
    return (
        <>
            <Breadcrumbs currentIconClass="far fa-bookmark" currentPageName={crud.pageName}>
                <Breadcrumb pageName={crud.listName} href={crud.listRouter}/>
                <Breadcrumb current={true} pageName={crud.pageName}/>
            </Breadcrumbs>
            {question && mounted && <div className="row">
                <div className="col-sm-12 mb-3">
                    <FormBootstrap autoComplete="off"
                                   className={"form js-form-new-" + crud.name}
                                   method="POST"
                                   noValidate
                                   validated={validated}
                                   onSubmit={handleSubmit}
                                   encType="multipart/form-data">
                        <TopActions backButton={{href: crud.listRouter}}
                                    submitButton={{value: crud.submitButton}}/>
                        {responseForm && <ResponseSuccessForm successMessage={responseForm}/>}
                        {responseFailForm && <ResponseFailForm errorMessage={responseFailForm}/>}

                        <Row text="Titulo de la pregunta">
                            <ColWithInput
                                col="col-md-8"
                                name="title"
                                label={false}
                                placeholder="Introduce un titulo"
                                defaultValue={question.title}
                                onChange={onChangeHandler}
                                required={true}
                            />
                        </Row>

                        <Answer data={question} setData={setQuestion}/>

                        <BottomActions backButton={{href: crud.listRouter}}
                                       submitButton={{value: crud.submitButton}}/>
                    </FormBootstrap>
                </div>
            </div> }
        </>
    )
}

export default QuestionsEdit;

Thanks again for your time.

@chiqui3d
Copy link

chiqui3d commented Jul 1, 2020

It has been complicated to me with the preview of the image, it is easier to leave it as it was before, that is to say that it is executed only once, that to have to be controlling the preview of the image and to come back to get the images through the DOM when sending the form. Seems to me more complicated.

@nfantone
Copy link

nfantone commented Jul 1, 2020

@chiqui3d Would you happen to have a runnable example you could share? It's kind of hard to follow what your issues are without something to collate against.

@pksorensen
Copy link

Similar problem i have: https://stackoverflow.com/questions/66699292/where-to-calculate-groups-for-fluentui-detaillist-in-nextjs

In fluentUI i want to calculate another state "groups" for a DetailedList based on returned data from swr, but then when user changes some state (similar to the input example) it then refetches with swr and then it recalculate the groups and i end up with revering the user action of collapsing UI.

New at all this - feeling I miss the bigger picture.

@shuding
Copy link
Member

shuding commented Sep 19, 2021

For whoever comes to this issue:

Since 1.0.0, there’s a new useSWRImmutable you can import from swr/immutable, which by default will only fetch the resource once. Related docs: https://swr.vercel.app/docs/revalidation#disable-automatic-revalidations.

@jonatanramhoj
Copy link

I'm new to using useSWR also but I'm actually having a similar use case as the author. I'm using Next.js and getStaticProps with incremental static regeneration.

When using ISR without SWR two refreshes are needed in order to see updates from the server (which is f*n annoying).

If I add SWR only one refresh is needed (expected behaviour for the user). A lot of my page content doesn't need to be revalidated on focus etc (just unnecessary requests for the users).

This is why I'm considering using SWR in a similar way as the author ie with most flags disabled.

@bethylogism
Copy link

This may help. SWR has excellent deduplication capacity built-in: https://swr.vercel.app/docs/advanced/performance

I was awaiting a lengthy promise.all with some moderate to hefty data manipulation on the results. I don't want to make these requests often, not even every time the component mounts as the data will at best be updated daily. I do want it to be cached and subtly revalidated on the semi-regular, though.

In my case, the above immutable config options plus revalidateOnMount: false ended up cacheing undefined against the key, even with a bit of manual mutate() going on.

What did work was setting the dedupingInterval to an hour.

{
	revalidateIfStale: false,
	revalidateOnFocus: false,
	revalidateOnReconnect: false,
	revalidateOnMount: true, // If false, undefined data gets cached against the key.
	dedupingInterval: 3_600_000, // dont duplicate a request w/ same key for 1hr
}

This way, SWR can complete its revalidation comparisons, which start when the component mounts and continue whilst the data's being manhandled, but the same API request isn't arbitrarily duplicated for at least an hour, even if the component remounts.

@fandyajpo
Copy link

fandyajpo commented Mar 16, 2022

i have an issue because my swr always fetching every time the animation moves, for example sidebar animation.
, if my page havent animation, that return no err or data fetch
my SWR config

<SWRConfig
value={{
fetcher: fetch,
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}}
>
<Component {...pageProps} />

@matsgm
Copy link

matsgm commented Jun 21, 2023

For late visitors just arriving... SWR now has "immutable mode": https://swr.vercel.app/blog/swr-v1.en-US#immutable-mode

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants