Recursion is a functional heritage and so using it with functional style yields the best results. That means writing functions that accept and operate on their inputs (rather than relying on external state) and return values (rather than relying on mutation or side effects).
Your program, on the other hand, calls functions without arguments, uses external state missingItems
, products
, productsLength
, total
, page
and uses mutations like page++
and reassignments like products = ...
, productsLength = ...
, missingItems = ...
. We're gonna fix all of this!
I'm just going to blast thru this and hope it sets you on the right track. If you're stuck at the end, I link some other answers which explain the techniques used here in greater detail.
const getAllProducts = async (page = 0) =>
asyncUnfold
( async (next, done, [ res, nextPage ]) =>
res.products.length === 0
? done ()
: next ( res.products // value to add to output
, [ await getPage (nextPage), nextPage + 1 ] // next state
)
, [ await getPage (page), page + 1 ] // initial state
)
We introduce the getPage
helper we used above
const getPage = async (page = 0, itemsPerPage = 5) =>
getProducts (page * itemsPerPage, itemsPerPage)
.then (res => res.json ())
Next, for the purposes of this demo, we introduce a fake getProducts
function, and a fake DB
where each product is simply a number. We also use delay
to simulate real network delay.
In your real program, you just need to provide a getProducts
function that can query products using offset
and limit
inputs
// fakes used for demonstration below
const getProducts = (offset = 0, limit = 1) =>
Promise.resolve
({ json: () =>
({ products: DB.slice (offset, offset + limit) })
})
.then (delay)
const delay = (x, ms = 250) =>
new Promise (r => setTimeout (r, ms, x))
const DB =
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
, 31, 32, 33
]
Below we demonstrate running the program. getAllProducts
is a familiar async function which returns a Promise of its result. We chain a .then
call so we can see all of the product pages output in the console
getAllProducts () .then (console.log, console.error)
// ~2 seconds later
// [ [ 1, 2, 3, 4, 5 ]
// , [ 6, 7, 8, 9, 10 ]
// , [ 11, 12, 13, 14, 15 ]
// , [ 16, 17, 18, 19, 20 ]
// , [ 21, 22, 23, 24, 25 ]
// , [ 26, 27, 28, 29, 30 ]
// , [ 31, 32, 33 ]
// ]
Instead of grouping products by page, it'd be nice if we could return all the products in a single array. We can modify getAllProducts
slightly to achieve this
const concat = (xs, ys) =>
xs .concat (ys)
const concatAll = (arrays) =>
arrays .reduce (concat, [])
const getAllProducts = async (page = 0) =>
asyncUnfold
( ... )
.then (concatAll)
getAllProducts () .then (console.log, console.error)
// ~2 seconds later
// [ 1, 2, 3, 4, 5, 6, 7, ..., 31, 32, 33 ]
Lastly, we introduce asyncUnfold
const asyncUnfold = async (f, initState) =>
f ( async (value, nextState) => [ value, ...await asyncUnfold (f, nextState) ]
, async () => []
, initState
)
Full program demonstration
// dependencies -------------------------------------------------
const asyncUnfold = async (f, initState) =>
f ( async (value, nextState) => [ value, ...await asyncUnfold (f, nextState) ]
, async () => []
, initState
)
const concat = (xs, ys) =>
xs .concat (ys)
const concatAll = (arrays) =>
arrays .reduce (concat, [])
// fakes --------------------------------------------------------
const getProducts = (offset = 0, limit = 1) =>
Promise.resolve
({ json: () =>
({ products: DB.slice (offset, offset + limit) })
})
.then (delay)
const delay = (x, ms = 250) =>
new Promise (r => setTimeout (r, ms, x))
const DB =
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30
, 31, 32, 33
]
// actual program
const getAllProducts = async (page = 0) =>
asyncUnfold
( async (next, done, [ res, nextPage ]) =>
res.products.length === 0
? done ()
: next ( res.products
, [ await getPage (nextPage), nextPage + 1 ]
)
, [ await getPage (page), page + 1 ]
)
.then (concatAll)
const getPage = async (page = 0, itemsPerPage = 5) =>
getProducts (page * itemsPerPage, itemsPerPage)
.then (res => res.json ())
// demo ---------------------------------------------------------
getAllProducts ()
.then (console.log, console.error)
// ~2 seconds later
// [ 1, 2, 3, ..., 31, 32, 33 ]