Suspense was introduced in the last article, so this one is about its good partner useTransition. If you’re a React fan, these are two posts you can’t miss.

We know React has done a lot of tweaking internally, but it also has some compact new apis that are designed to optimize the user experience. React officials have a long document dedicated to this aspect of motivation and creativity called Concurrent UI Patterns, in which the main character is useTransition.


Related articles

  • This is probably the most popular way to open the React Fiber 🔥 enter the door first
  • Suspense the World!




This article outline

  • What are the application scenarios?
  • UseTransition appearance
  • A preliminary study of the useTransition principle
    • 1️ Uses startTransition to run low-priority tasks
    • 2️ startTransition update triggers Suspense
    • 3️ Mentions tick updates outside the startTransition range
    • 4 ️ ⃣ nested Suspense
    • 5 Can ️ be used with Mobx and Redux?
  • The useDeferedValue?
  • conclusion
  • The resources


React uses’ parallel universes’ as a metaphor for the useTransition API. What?

The React branch can Fork a new branch (still called Pending) from the current view (which can be considered as the Master). Updates are made on this new branch while the Master keeps responding and updating. The two branches are like ‘parallel universes’ where they don’t interfere with each other. When the Pending branch is’ ready ‘, merge (commit) to the Master branch.


UseTransition acts like a time tunnel, allowing components to enter a parallel universe where they wait for asynchronous states (asynchronous request, delay, whatever) to be ready. In parallel universes, useTranstion can be configured with a timeout period. If the timeout period expires, the asynchronous state will be forcibly pulled back to the real world even if it is not ready. Back in the real world, React immediately merges changes to component Pengding and presents them to users.

Thus, you can think of the React component in Concurrent mode as having three states:


  • Normal – Components in the Normal state
  • Suspense – Components that hang because of asynchronous state
  • Pending – Components that enter parallel universes. There are also Pending ‘state changes’ that React doesn’t commit to the UI immediately but are cached for Suspense ready or time out.

You may not understand this yet, but that’s ok. Keep reading.




What are the application scenarios?

What’s the use of parallel universes? We’re not talking about code or architecture level stuff. Just look at the UI: In some UI interaction scenarios, we don’t want to apply changes to the page right away.

🔴 such as you switch from one page to another page, the new page may need some time to complete the loading, we stay on a page, a little more willing to maintain some response operation, for example, we can cancel, or perform other operations, and give me a blank page or nothing idle load state, feel doing unnecessary waiting.

This kind of interaction is very common, and here’s an example:


Pretend I can afford AirPods


And Github:

Some famous dating website abroad


For example, if I click to buy AirPods, the browser will stay on the previous page until the next page request is answered or times out. In addition, the browser prompts the request with a load indicator in the address bar. This interaction design is much better than simply switching to a blank page. The page can hold the user response, or it can cancel the request at any time and remain on the original page.

And, of course, there’s another interaction when you’re tabbing, and we want it to go right away, otherwise the user will think that the click doesn’t work.

Another benefit of ‘parallel universes’ : 🔴 We assume that most of the time data requests are very fast, and there is no need to show the loading state, which would cause pages to flicker and wobble. In fact, by a short delay, you can reduce the frequency of loading state display.

Alternatively, 🔴useTransition can also be used to wrap low-priority updates. As it stands, React does not intend to expose too much low-level detail about Concurrent modes. If you want to schedule low-priority updates, use useTransition only.




UseTransition appearance


As shown above, we first define the various states of the page as described in the React official document. It mentions the following three stages of page loading:

(1) The Transition period

This is when the page is not ready, waiting for critical data to load. Depending on the presentation strategy, the page can have the following two states:

  • ⚛️ degradation (Receded). Immediately switch the page over to show a large load indicator or a blank page. What does’ degenerate ‘mean? According to React, the fact that a page once had content is now content-free is a kind of degeneration, or ‘retrograde’ to history.

  • ⚛️ Pending. This is the state that useTransition is trying to achieve, which is to stay on the current page and keep the current page responsive. Enter the Skeleton state when critical data is ready, or wait for timeout and fall back to the Receded state.


② Loading stage

This is when the key data is ready to start presenting the skeleton or frame portion of the page. This phase has a state:

  • ⚛️ Skeleton (Skeleton). The key data has been loaded, and the page shows the framework of the body.


(3) The Done stage.

This means that the page is fully loaded. This phase has a state:

  • ⚛️ Complete rendering the page completely




Set Receded -> threshold -> Complete 🔴 recede -> threshold -> Complete when changing state to a new screen. Before this, it was difficult to implement the 🔴Pending -> Skeleton -> Complete load path. UseTransition can change that.


Let’s simply simulate a page switch by looking at how it loads by default:

function A() {
  return <div className="letter">A</div>;
}

function B() {
  // ⚛️ Lazy load for 2s to simulate asynchronous data requests
  delay("B".2000);
  return <div className="letter">B</div>;
}

function C() {
  // ⚛️ Loads 4s lazily to simulate asynchronous data requests
  delay("C".4000);
  return <div className="letter">C</div>;
}

/ / page 1
function Page1() {
  return <A />; Function Page2() {return ();<>
      <B />
      <Suspense fallback={<div>Loading... C</div>} ><C />
      </Suspense>
    </>); } function App() { const [showPage2, setShowPage2] = useState(false); Const handleClick = () => setShowPage2(true) Return (<div className="App">
      <div>
        <button onClick={handleClick}>switch</button>
      </div>
      <div className="page">
        <Suspense fallback={<div>Loading ...</div>} > {! showPage2 ?<Page1 /> : <Page2 />}
        </Suspense>
      </div>
    </div>
  );
}
Copy the code


Here’s how it works:

After clicking on the switch, we will immediately see a large Loading… Then B is loaded after 2s, and C is loaded after 2s. Receded -> Skeleton -> Complete


Now it’s time for useTransition 🎉, a simple modification of the above code:

// ⚛️ Import useTransition
import React, { Suspense, useState, useTransition } from "react";

function App() {
  const [showPage2, setShowPage2] = useState(false);
  // ⚛️ useTransition receives a timeout, returns a startTransition function, and a pending function
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = (a)= >
    // ⚛️ wraps state changes that might trigger Suspense hang in startTransition
    startTransition((a)= > {
      setShowPage2(true);
    });

  return (
    <div className="App">
      <div>
        <button onClick={handleClick}>switch</button>{/* ⚛️ pending indicates that the pending state is pending. You can do some slight prompt */} {pending &&<span>In the switch...</span>}
      </div>
      <div className="page">
        <Suspense fallback={<div>Loading ...</div>} > {! showPage2 ?<Page1 /> : <Page2 />}
        </Suspense>
      </div>
    </div>
  );
}
Copy the code


The useTransition Hook API is simple and has four key points:

  • TimeoutMs indicates the timeout time for switching (the longest that exists in parallel universes). UseTransition keeps React on the current page until it is triggered by Suspense ready or timeout.

  • StartTransition, which encapsulates state changes that might trigger page transitions (technically trigger Suspense hangs), actually provides an ‘updated context’. We’ll explore this in more detail in the next video

  • Pending: indicates the pending state. We can use this status value to prompt the user appropriately.

  • Suspense, useTransition states must work with Suspense, that is, updates in startTransition must trigger any Suspense hang.


Take a look at this in action!


You can see it in action in this CodeSandbox

This will have exactly the same effect as the ‘first image’ at the beginning of this section: React will remain on the current page, pending will change to true, and then B will be ready and the screen will immediately switch over. Pending -> Skeleton -> Complete

Once changes in startTransition are triggered in Suspense, React will put the Pending state of change markers, and React will delay ‘submitting’ those changes. So there really isn’t a parallel universe, which is all that fancy and magical, and React just delayed the submission of those changes. All we see on the interface is the old or unpending state. React prerenders in the background.

Note that React simply did not commit these changes, which does not mean that React is’ stuck ‘. Components in the Pending state can also receive responses from users and make new state changes. New status updates can override or terminate Pending states.


To summarize the conditions for entering and exiting the Pending state:

  • Enter the PendingThe state first needs to beState changesWrapped instartTransitionThese updates trigger Suspense hangs
  • There are three ways to exit the Pending state: ① Suspense ready; (2) overtime; ③ Overwritten or terminated by a new status update




A preliminary study of the useTransition principle

In this section, we’ll take a closer look at useTransition, but not by fiddling with the source code, but by treating it like a black box and using a few experiments to deepen your understanding of useTransition.

UseTransition, formerly known as withSuspenseConfig, was introduced by Sebmarkbage in a PR proposed last May.

In naming terms, it just wants to configure Suspense. We can also verify this with the latest source code. UseTransition’s job ‘seems’ to be very simple:

function updateTransition( config: SuspenseConfig | void | null, ): [(() => void) => void, boolean] { const [isPending, setPending] = updateState(false); // Equivalent to useState const startTransition = updateCallback(// equivalent to useCallback callback => {setPending(true); Unstable_next (() => {// ⚛️ Set "suspenseConfig const previousConfig" = ReactCurrentBatchConfig.suspense; ReactCurrentBatchConfig.suspense = config === undefined ? null : config; Try {// Restore pending setPending(false); // Execute your callback(); } the finally {/ / ⚛ ️ reduction suspenseConfig ReactCurrentBatchConfig. Suspense = previousConfig; }}); }, [config, isPending], ); return [startTransition, isPending]; }Copy the code


It seems so ordinary. What’s the point? Sebmarkbage also mentioned some information in the above PR.

  • StartTransition sets pending to true as soon as execution begins. The callback is then executed using UNstable_NEXT, which lowers the priority of the update. This means that the ‘change’ triggered by the UNstabLE_NEXT callback will have a lower priority and give way to a higher-priority update, or if the current transaction is busy, it will be scheduled to be applied in the next idle period, but it may also be applied immediately.

  • Configuration. The point is to ReactCurrentBatchConfig suspense, there will be configuration of suspense timeout. It indicates that changes triggered by this interval are associated with the suspenseConfig, and that these changes will have their expiredTime(which can be regarded as’ priority ‘) calculated against the suspenseConfig. For the time being, these changes associated with suspenseConfig are referred to as Pending changes.

  • The Pending change triggers a Render that is also associated with the suspenseConfig. If Suspense is triggered during rendering, Pending changes will be delayed commit, cached in memory and forced to be committed to the user interface until it times out or is ready, or is overwritten by other updates.

  • Pending changes are only committed late, but do not affect the consistency of the final data and view. React will be rerendered in memory, just not submitted to the user interface.


The React internal implementation is so complex that I found it expensive to dig it up or put it into words. So another way to understand its behavior is through experiments (black boxes) :

So the experimental code is in this CodeSandbox


1️ Uses startTransition to run low-priority tasks

This experiment is mainly used to verify that unstable_next will reduce the priority of updates. In the following experiment, we will observe that changes to the package via startTransition are updated slightly later when the task is busy, but the final state is consistent.

Experimental code:

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = (a)= > {
    // ⚛️ Update synchronously
    setCount(count + 1);

    startTransition((a)= > {
      // ⚛️ Update the tick with a low priority
      setTick(t= > t + 1);
    });
  };

  return (
    <div className="App">
      <h1>Hello useTransition</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span>pending</span>}
      </div>
      <div>Count: {count}</div>{/* ⚛️ This is a complex component that takes a little time to render and simulates busy situations */}<ComplexComponent value={tick} />
    </div>
  );
}
Copy the code


The experimental results are as follows:


In the case of consecutive clicks, the ComplexComponent updates are significantly delayed because the tick changes are delayed and merged, but in the end their results are consistent.




2️ startTransition update triggers Suspense

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const handleClick = (a)= > {
    startTransition((a)= > {
      setCount(c= > c + 1);
      setTick(c= > c + 1);
    });
  };

  return( <div className="App"> <h1>Hello useTransition {tick}</h1> <div> <button onClick={handleClick}>ADD + 1</button> {pending && <span className="pending">pending</span>} </div> <Tick /> <SuspenseBoundary id={count} /> </div> ); } const SuspenseBoundary = ({ id }) => { return ( <Suspense fallback="Loading..." > {/ * this throws a Promise is unusual, resolved after 3 s * /} < ComponentThatThrowPromise id = {id} / > < / Suspense >); }; Const Tick = ({duration = 1000}) => {const [Tick, setTick] = useState(0); useEffect(() => { const t = setInterval(() => { setTick(tick => tick + 1); }, duration); return () => clearInterval(t); }, [duration]); return <div className="tick">tick: {tick}</div>; };Copy the code

Suspense is triggered when the count and tick are incremented when we click the button, and the count is passed to the SuspenseBoundary.

As you can see from the above results, changes have been made in startTransition (with suspenseConfig) and the corresponding re-rendering triggers Suspense, so it goes into the Pending state, where the rendered results are not immediately ‘committed’ and the page remains in its original state.

In addition, you will find that the App component’s tick will be ‘stopped’ just like the ‘SuspenseBoundary’ (see the tick after the Hello Transition), because the tick change is also associated with the ‘SuspenseBoundary’.

The Tick component increments every second and does not block.

This means that once Suspense is triggered, changes that are associated with suspenseConfig will be ‘suspended’ for submission.




3️ Mentions tick updates outside the startTransition range

On the basis of 2️, setTick is mentioned outside the startTransition scope:

export default function App() {
  const [count, setCount] = useState(0);
  const [tick, setTick] = useState(0);
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  console.log("App rendering with", count, tick, pending);

  const handleClick = (a)= > {
    setTick(c= > c + 1);
    startTransition((a)= > {
      setCount(c= > c + 1);
    });
  };

  const handleAddTick = (a)= > setTick(c= > c + 1);

  useEffect((a)= > {
    console.log("App committed with", count, tick, pending);
  });

  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        <button onClick={handleAddTick}>Tick + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />
      <SuspenseBoundary id={count} />
    </div>
  );
}
Copy the code


Now the tick will be updated immediately and the SuspenseBoundary will remain in the pending state.

Let’s open the console and look at the output:

App rendering with 1 2 True # pending is set to true, count this time is 1, Tick = 2 App rendering with 1 2 true read 1 App committed with 1 2 true #
#Below the Tick is updated three times (3s)
#We notice that each time React rerenders the App component, i.e'ping'Take a look at a component in the Pending state and check to see if it is' ready '(not triggering Suspense)
#If Suspense is also triggered, indicating that you still have to wait, these rerendered results will not be submittedApp Rendering with 2 2 false # ping where count becomes 2 and pending becomes false App rendering with 2 2 false # but React renders them in memory Read 2 Tick Rendering with 76 # Tick Rendering with 76 # Tick committed App rendering with 2 2 false # ping Pending App Rendering with 2 2 false read 2 Tick rendering with 77 Tick rendering with 77 Tick committed with 77 App rendering with 2 2 false # ping App rendering with 2 2 false read 2 Tick rendering with 78 Tick rendering with 78 Tick committed with 78 App rendering with 2 2 false # ping App rendering with 2 2 false read 2
#Ok, the Promise is ready. At this point, re-render the App again
#Suspense isn't triggered this time, React will immediately submit the UI
App rendering with 2 2 false
App rendering with 2 2 false
read  2
App committed with 2 2 false
Copy the code

Through the above log, we can clearly understand the update behavior of Pending components




4 ️ ⃣ nested Suspense

On the basis of 3️, SuspenseBoundary is changed to DoubleSuspenseBoundary, and a more time-consuming resource for Suspense loading will be nested here:

const DoubleSuspenseBoundary = ({ id }) = > {
  return( <Suspense fallback={<div>Loading... < / div >} > {/ * need to load 2 s * /} < ComponentThatThrowPromise id = timeout = {2000} / {id} > < Suspense fallback = {< div > Loading second... < / div >} > {/ * need to load 4 s * /} < ComponentThatThrowPromise id = {id + "second"} timeout = {4000} / > < / Suspense > < / Suspense >)}Copy the code

To test the effect:


Watch first for first mounts. Suspense doesn’t trigger delayed commits for first mounts, so the first thing we see is Loading… , then the first ComponentThatThrowPromise finishes Loading, display ComponentThatThrowPromise id: 0 and Loading the second… , and finally fully loaded.

We then click on the button, then DoubleSuspenseBoundary will remain motionless, waiting for after the 5 s (i.e., the second ComponentThatThrowPromise loaded), to submit.


Ideal result is the same as when mount for the first time: in the first ComponentThatThrowPromise ready when they switch to come over, don’t have to wait for a second to load.

Doesn’t feel right? Concurrent UI Patterns (Experimental) -wrap Lazy Features in

The second ComponentThatThrowPromise already nested in Suspense, theoretically should not block the submission.

Back to the first sentence: ‘Suspense does not trigger late commits when first mounted ‘. Let’s try again, set a key to DoubleSuspenseBoundary and force it to destroy and recreate:

export default function App() {
  / /...
  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />{/* ⚛️ add key, force re-destruct create */}<DoubleSuspenseBoundary id={count} key={count} />
    </div>)}Copy the code


Try the effect:


We find that every click is Loading… The Pending state is gone! Because each time the count is incremented, the DoubleSuspenseBoundary is recreated and no late commit is triggered.

Based on this principle, we can remake the DoubleSuspenseBoundary again, this time by simply adding keys to nested Suspense so that they can recreate a non-blocking Pending state.

const DoubleSuspenseBoundary = ({ id }) = > {
  return( <Suspense fallback={<div>Loading... < / div >} > < ComponentThatThrowPromise id = timeout = {2000} / {id} > {/ * ⚛ ️ we don't want this Suspense blocked pending status, give it a key, Suspense key={id} fallback={<div>Loading second... </div>}> <ComponentThatThrowPromise id={id + "second"} timeout={4000} /> </Suspense> </Suspense> ); };Copy the code


The final effect

It’s work! 🍻




5 Can ️ be used with Mobx and Redux?

I don’t know. Here’s a test:

mport React, { useTransition, useEffect } from "react";
import { createStore } from "redux";
import { Provider, useSelector, useDispatch } from "react-redux";
import SuspenseBoundary from "./SuspenseBoundary";
import Tick from "./Tick";

const initialState = { count: 0.tick: 0 };
const ADD_TICK = "ADD_TICK";
const ADD_COUNT = "ADD_COUNT";

const store = createStore((state = initialState, action) = > {
  constcopy = { ... state };if (action.type === ADD_TICK) {
    copy.tick++;
  } else {
    copy.count++;
  }
  return copy
});

export const Page = (a)= > {
  const { count, tick } = useSelector(({ tick, count }) = > ({ tick, count }));
  const dispatch = useDispatch();
  const [startTransition, pending] = useTransition({ timeoutMs: 10000 });

  const addTick = (a)= > dispatch({ type: ADD_TICK });
  const addCount = (a)= > dispatch({ type: ADD_COUNT });

  const handleClick = (a)= > {
    addTick();
    startTransition((a)= > {
      console.log("Start transition with count: ", count);
      addCount();
      console.log("End transition");
    });
  };

  console.log(`App rendering with count(${count}) pendig(${pending}) `);

  useEffect((a)= > {
    console.log("committed with", count, tick, pending);
  });

  return (
    <div className="App">
      <h1>Hello useTransition {tick}</h1>
      <div>
        <button onClick={handleClick}>ADD + 1</button>
        {pending && <span className="pending">pending</span>}
      </div>
      <Tick />
      <SuspenseBoundary id={count} />
    </div>
  );
};

export default () => {
  return (
    <Provider store={store}>
      <Page />
    </Provider>
  );
};

Copy the code


Here’s how it works:



What ‘s the problem? The whole interface is Pending, not only the App subtree, but also the Tick is not going. Open the console and see a warning:

Warning: Page triggered a user-blocking update that suspended.

The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes.
Refer to the documentation for useTransition to learn how to implement this pattern.
Copy the code


Let’s take a look at how the current Rudux Hooks API and Mobx Hooks API are updated. Essentially, they use a subscription mechanism to force updates after an event is triggered.

function useSomeOutsideStore() {
  // Get the external store
  const store = getOutsideStore()
  const [, forceUpdate] = useReducer(s= > s + 1.0)

  // ⚛️ Subscribe to external data sources
  useEffect((a)= > {
    const disposer = store.subscribe((a)= > {
      // ⚛️ Forcibly update
      forceUpdate()
    ))

    return disposer
  }, [store])

  // ...
}
Copy the code


That is, when we update the Redux state in startTransition, we receive an event synchronously and then call forceUpdate. ForceUpdate is the status that really changes in the context of suspenseConfig.

Let’s look at the console log again:

Start Transition with count 0 End Transition App rendering with count(1) pendig(true) You can compare App rendering with count(1) pendig(true) on experiment 3️, where the count is 0 and the count is 1, meaning there has been no defer! read 1 Warning: App triggered a user-blocking update that suspended. The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes. Refer to the documentation for useTransition to learn how to implement this pattern.Copy the code

It’s basically possible to locate problems through logging, and count is not being updated late, causing ‘syncing’ to trigger Suspense, which is why React warnings are being issued. Since useTransition is currently in the experimental stage, the behavior would still be undetermined if it weren’t for state updates in the startTransition context.

But the final behavior is a bit metaphysical; it causes the entire application to be ‘Pending’ and no status updates to be committed. I am also very confused about this part, and have no energy to go into it. I can only wait for the subsequent official update, which readers can also ponder over.

For this reason, it is not recommended for Suspense states to trigger placement in Redux or Mobx for the time being.




For the useTransition to enter Pending, the following conditions must be met:

  • It’s best to use React’s own state mechanisms, such as Hooks or setState, instead of Mobx and Redux for now
  • These updates trigger Suspense.
  • Updates must be made instartTransitionIn scope, these updates are associatedsuspenseConfig
  • At least one of these updates must trigger a re-renderSuspense
  • thisSuspenseThis is not the first mount




The useDeferedValue?

If you understand the above, then useDeferedValue is a simple wrapper around useTransition:

function useDeferredValue<T> (value: T, config: TimeoutConfig | void | null,) :T {
  const [prevValue, setValue] = useState(value);
  const [startTransition] = useTransition(config)

  // ⚛️ useDeferredValue simply listens for changes in value,
  // Then update it in startTransition. Thus, the effect of delayed update is realized
  useEffect(
    (a)= > {
      startTransition((a)= > {
        setValue(value);
      })
    },
    [value, config],
  );

  return prevValue;
}
Copy the code


UseDeferredValue simply listens for changes in value using useEffect and updates it in startTransition. Thus, the effect of delayed update is realized. Experiment 1️ has already introduced the operation effect. React will reduce the priority of updates in startTransition, which means that they will be delayed when transactions are busy.




conclusion

At the beginning, we introduced the application scenario of useTransition, which makes the page realize the update path Pending -> Skeleton -> Complete, and the user can stay on the current page when switching pages, so that the page remains responsive. This is a much more user-friendly experience than showing a blank page or loading state that is useless.

Of course, the assumption is that the data load is slow. If the data load is fast, we use the useTransition mechanism to prevent the user from seeing the loading state, so that the page will not shake and flicker, and it will look like the loading process is not happening.

Then we briefly introduce the operation principle and conditions of useTransition. If state updates in startTransition trigger Suspense, the corresponding component goes into the Pending state. During the Pending state, setting changes in startTransition are deferred. Pending states persist until Suspense is ready or time out.

UseTransition must be used with Suspense to work its magic. There is another user scenario where we can put low-priority updates into startTransition. For example, if an update is expensive, you can choose to put it in startTransition. These updates will make way for higher-priority tasks, and React will delay or merge a more complex update to keep the page responsive.




Ok, let’s stop talking about Concurrent mode. This is first-hand information in Chinese. Writing these articles consumes most of my spare time, if you like my articles, please give me more likes and feedback.




The resources

  • Concurrent UI Patterns
  • Add withSuspenseConfig API