React Function params are undefined - reactjs

I am still new to React and am trying to update my api with a fetch request that takes the values provided by my "form". I'm trying to pass in a variable as a parameter to a function but its undefined.
I'm thinking it may be something to do with it being asynchronous but I'm not entirely sure.
import './MakeRequest.css';
import React, { useState , useEffect } from 'react'
import { useLocation } from "react-router-dom";
const MakeRequest = () => {
const [partySize, setPartySize] = useState(0);
const [time, setTime] = useState("");
const [expiration_date, setExpirationDate] = useState("");
const addRequests = async ({restaurant,partySize,time,expiration_date}) => {
//values are undefined expect restaurant_name
console.log(partySize)
console.log(time)
console.log(expiration_date)
let item={"restaurant":restaurant.restaurant_name,"party_size":partySize,"time": time,"expiration_date":expiration_date}
let result = await fetch("api thingy", {
method: 'POST',
headers:{
"Content-Type":"application/json",
"Accept":'application/json'
},
body:JSON.stringify(item)
})
result = await result.json();
};
const location = useLocation();
const restaurant = location.state;
return (
<div className = "makeRequest">
<img src={restaurant.picture} alt="" className="requestpicture"/>
<h1> Reservation at {restaurant.restaurant_name} </h1>
<h2> {restaurant.address} </h2>
<input type = "number" placeholder = "Party Size" id = "party-size"
onChange = {(e)=>setPartySize(e.target.value)}
className = "form-control"/>
<input type = "time" placeholder = "Time" id = "time"
onChange = {(e)=>setTime(e.target.value)}
className = "form-control"/>
<input type = "date" placeholder = "Expiration Date" id = "expiration-date"
onChange = {(e)=>setExpirationDate(e.target.value)}
className = "form-control"/>
{/*
<button onClick={() => console.log(partySize)}> Make Request </button>
*/}
<button onClick={() => addRequests(restaurant,partySize,time,expiration_date)}> Make Request </button>
</div>
)}
export default MakeRequest;
I tried checking to see if for some reason the values are never set in the first place. but when I run the commented code:
<button onClick={() => console.log(partySize)}> Make Request </button>
I actually receive the right values. Why is this so?

You're calling addRequests with 5 arguments, but your function expects a single object.
You could either remove the curlies from the function declaration:
const addRequests = async (restaurant,partySize,time,expiration_date) => {
Or add them to the place where you call it:
onClick={() => addRequests({restaurant,partySize,time,expiration_date})}
Additional Info
Functions that take many arguments can be annoying to use because you have to remember what order to pass them in:
// args must be passed in this order
function addRequests (restaurant, partySize, time, expiration_date) { ... }
To avoid this problem it's common to declare functions that take a single argument, an object with named properties:
function addRequests(options) {}
Declared this way the function can be invoked without having to think about argument order:
const request = {
partySize: 5,
expiration_date: new Date(),
time: '7:00',
restaurant: {
name: 'Pizza Chunks',
address: '999 Chonky Chonk Way'
}
}
addRequests(request);
The function can then extract the info it needs by name:
function addRequests(options) {
const time = options.time; // '7:00'
const partySize = options.partySize; // 5
...
}
It can extract a bunch of options at once via destructuring. This is equivalent to the previous snippet:
function addRequests(options) {
const { time, partySize } = options;
...
}
To tighten up the code even further, you can do the destructuring on incoming argument. The function still takes a single argument. We're just extracting properties from that argument. Again, this is equivalent to the examples above.
function addRequests({ time, partySize }) {
...
}
shorthand property names
One other thing crucial to understanding the effect the curly braces have when invoking your function is "shorthand property names". When declaring an object literal you can omit the "value" for properties where you'd just be repeating the name:
const partySize = 5;
const time = '7:00';
const options = {
partySize,
time,
}
// same as
const options = {
partySize: partySize,
time: time,
}
So when you do this (with the curly braces):
addRequests({ restaurant, partySize, time, expiration_date })
You're passing a single argument, an object, with those fields.
Without the curlies you're passing 4 arguments, in a specific order:
addRequests( restaurant, partySize, time, expiration_date )
You can make it work either way, but you have to decide whether the function takes multiple positional arguments or a single options object argument.
You can of course do both if you want:
function someFunc(foo, options) { ... }
someFunc(24, { bing, bang, boom })

Related

Takes two clicks for react bootstrap popover to show up

I've run into an issue while trying to build a page that allows the user to click on a word and get its definition in a bootstrap popover. That is achieved by sending an API request and updating the state with the received data.
The problem is that the popover only appears after the second click on the word. The console.log() in useEffect() shows that every time a new word is clicked an API request is made. For the popover to appear the same word must be clicked twice. It'd be better if it only took one click.
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
export default function App() {
const [text, setText] = useState(
"He looked at her and saw her eyes luminous with pity."
);
const [selectedWord, setSelectedWord] = useState("luminous");
const [apiData, setApiData] = useState([
{
word: "",
phonetics: [{ text: "" }],
meanings: [{ definitions: [{ definition: "", example: "" }] }]
}
]);
const words = text.split(/ /g);
useEffect(() => {
var url = "https://api.dictionaryapi.dev/api/v2/entries/en/" + selectedWord;
axios
.get(url)
.then(response => {
setApiData(response.data)
console.log("api call")
})
.catch(function (error) {
if (error) {
console.log("Error", error.message);
}
});
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const popover = (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</Popover.Body>
</Popover>
);
return (
<Alert>
{words.map((w) => (
<OverlayTrigger
key={uuid()}
trigger="click"
placement="bottom"
overlay={popover}
>
<span onClick={() => clickCallback(w)}> {w}</span>
</OverlayTrigger>
))}
</Alert>
);
}
UPDATE:
Changed the apiData initialization and the <Popover.Body> component. That hasn't fixed the problem.
const [apiData, setApiData] = useState(null)
<Popover.Body>
{
apiData ?
<div>
<h1>{apiData[0].word}</h1>
<h6>{apiData[0].meanings[0].definitions[0].definition}</h6>
</div> :
<div>Loading...</div>
}
</Popover.Body>
The Problem
Here's what I think is happening:
Component renders
Start fetching definition for "luminous".
The definition of "luminous" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "luminous", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("luminous") does nothing.
If you click another word, such as "pity", the popper attempts to show, but setSelectedWord("pity") causes the component to start rerendering.
Component rerenders
Start fetching definition for "pity".
The definition of "pity" has finished being fetched. It calls setApiData(data).
Component rerenders
If you click "pity", the popper is shown immediately, this is because the data for the popper is ready to use and setSelectedWord("pity") does nothing.
Selecting another word will repeat this process over and over.
To fix this, you need to first make use of the show property to show the popover after rendering it out if it matches the selected word. But what if the word appears multiple times? If you did this for the word "her", it would show the popover in multiple places. So instead of comparing against each word, you'd have to assign each word a unique ID and compare against that.
Fixing the Component
To assign words an ID that won't change between renders, we need to assign them IDs at the top of your component and store them in an array. To make this "simpler", we can abstract that logic into a re-useable function outside of your component:
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading punctuation
.replace(/[.,!?"]+$/, "") // remove trailing punctuation
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
// we can use this to prevent unnecessary rerenders
words.text = text;
// return the array-object
return words;
}
Within the component, we need to setup the state variables to hold the words array. By passing a callback to useState, React will only execute it on the first render and skip calling it on rerenders.
// set up state array of words that have their own UUIDs
// note: we don't want to call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
Now that we have words and _setWords, we can pull out the text value from it:
// extract text from words array for convenience
// probably not needed
const text = words.text;
Next, we can create our own setText callback. This could be simpler, but I wanted to make sure we support React's mutating update syntax (setText(oldValue => newValue)):
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
Next, we need to set up the currently selected word. Once the definition is available, this word's popover will be shown.
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
If you don't want to show a word by default, use:
const [selectedWordObj, setSelectedWordObj] = useState(); // nothing selected by default
To fix the API call, we need to make use of the "use async effect" pattern (there are libraries out there to simplify this):
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
The above code block makes sure to prevent calling the setApiData methods when they aren't needed any more. It also uses a status property to track it's progress so you can render the result properly.
Now to define a popover that shows a loading message:
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
We can mix that loading popover with apiData to get a popover to show the definition. If we're still loading the definition, use the loading one. If we've had an error, show the error. If it completed properly, render out the defintion. To make this easier, we can put this logic in a function outside of your component like so:
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
We call this funtion in the component using:
const selectedWordPopover = getPopover(apiData, loadingPopover);
Finally, we render out the words. Because we are rendering out an array, we need to use a key property that we'll set to each word's Id. We also need to select the word that was clicked - even if there were more than one of the same words, we only want just the clicked one. For that we'll check its Id too. If we click on a particular word, we need to sure that the one we clicked on is selected. We also need to render out the original word with its punctuation. This is all done in this block:
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
Complete Code
Bringing all that together gives:
import React, { useState, useRef, useEffect } from "react";
import axios from "axios";
import { Alert, Popover, OverlayTrigger } from "react-bootstrap";
// Use this function snippet in demos only, use a more robust package
// https://gist.github.com/jed/982883 [DWTFYWTPL]
const uuid = function b(a){return a?(a^Math.random()*16>>a/4).toString(16):([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g,b)}
// Splits the text argument into words, removes excess formatting characters and assigns each word a UUID.
// Returns an array with the shape: { [index: number]: { word: string, original: string, uuid: string }, text: string }
function identifyWords(text) {
// split input text into words with unique Ids
const words = text
.split(/ +/)
.map(word => {
const cleanedWord = word
.replace(/^["]+/, "") // remove leading characters
.replace(/[.,!?"]+$/, "") // remove trailing characters
return { word: cleanedWord, original: word, uuid: uuid() }
});
// attach the source text to the array of words
words.text = text;
// return the array
return words;
}
function getPopover(apiData, loadingPopover) {
switch (apiData.status) {
case "loading":
return loadingPopover;
case "error":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>Couldn't find definition for {apiData.word}: {apiData.error.message}</h6>
</Popover.Body>
</Popover>
);
case "completed":
return (
<Popover id="popover-basic">
<Popover.Body>
<h1>{apiData.word}</h1>
<h6>{apiData.definition}</h6>
</Popover.Body>
</Popover>
);
}
}
export default function App() {
// set up state array of words that have their own UUIDs
// note: don't call _setWords directly
const [words, _setWords] = useState(() => identifyWords("He looked at her and saw her eyes luminous with pity."));
// extract text from words array for convenience
const text = words.text;
// mimic a setText callback that actually updates words as needed
const setText = (newTextOrCallback) => {
if (typeof newTextOrCallback === "function") {
// React mutating callback mode
_setWords((words) => {
const newText = newTextOrCallback(words.text);
return newText === words.text
? words // unchanged
: identifyWords(newText); // new value
});
} else {
// New value mode
return newTextOrCallback === words.text
? words // unchanged
: identifyWords(newTextOrCallback); // new value
}
}
const [selectedWordObj, setSelectedWordObj] = useState(() => words.find(({word}) => word === "luminous"));
const [apiData, setApiData] = useState({ status: "loading" });
useEffect(() => {
if (!selectedWordObj) return; // do nothing.
// TODO: check cache here
// clear out the previous definition
setApiData({ status: "loading" });
let unsubscribed = false;
axios
.get(`https://api.dictionaryapi.dev/api/v2/entries/en/${selectedWordObj.word}`)
.then(response => {
if (unsubscribed) return; // do nothing. out of date response
const body = response.data;
// unwrap relevant bits
setApiData({
status: "completed",
word: body.word,
definition: body.meanings[0].definitions[0].definition
});
})
.catch(error => {
if (unsubscribed) return; // do nothing. out of date response
console.error("Failed to get definition: ", error);
setApiData({
status: "error",
word: selectedWordObj.word,
error
});
});
return () => unsubscribed = true;
}, [selectedWord]);
function clickCallback(w) {
var word = w.split(/[.!?,]/g)[0];
setSelectedWord(word);
}
const loadingPopover = (
<Popover id="popover-basic">
<Popover.Body>
<span>Loading...</span>
</Popover.Body>
</Popover>
);
const selectedWordPopover = getPopover(apiData, loadingPopover);
return (
<Alert>
{words.map((wordObj) => {
const isSelectedWord = selectedWordObj && selectedWordObj.uuid = wordObj.uuid;
return (
<OverlayTrigger
key={wordObj.uuid}
show={isSelectedWord}
trigger="click"
placement="bottom"
overlay={isSelectedWord ? selectedWordPopover : loadingPopover}
>
<span onClick={() => setSelectedWordObj(wordObj)}> {wordObj.original}</span>
</OverlayTrigger>
)})}
</Alert>
);
}
Note: You can improve this by caching the results from the API call.

Dynamically create React.Dispatch instances in FunctionComponents

How can I create an array of input elements in react which are being "watched" without triggering the error for using useState outside the body of the FunctionComponent?
if I have the following (untested, simplified example):
interface Foo {
val: string;
setVal: React.Dispatch<React.SetStateAction<string>>;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
const [val, setVal] = useState('')
setAllVals(allVals.concat({val, setVal}))
}
return (
<input type="button" value="Add input" onClick={addVal}>
allVals.map(v => <li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>)
)
}
I will get the error Hooks can only be called inside of the body of a function component.
How might I dynamically add "watched" elements in the above code, using FunctionComponents?
Edit
I realise a separate component for each <li> above would be able to solve this problem, but I am attempting to integrate with Microsoft Fluent UI, and so I only have the onRenderItemColumn hook to use, rather than being able to create a separate Component for each list item or row.
Edit 2
in response to Drew Reese's comment: apologies I am new to react and more familiar with Vue and so I am clearly using the wrong terminology (watch, ref, reactive etc). How would I rewrite the code example I provided so that there is:
An add button
Each time the button is pressed, another input element is added.
Each time a new value is entered into the input element, the input element shows the value
There are not excessive or unnecessary re-rendering of the DOM when input elements have their value updated or new input element is added
I have access to all the values in all the input elements. For example, if a separate submit button is pressed I could get an array of all the string values in each input element. In the code I provided, this would be with allVals.map(v => v.val)
const [val, setVal] = useState('') is not allowed. The equivalent effect would be just setting value to a specific index of allVals.
Assuming you're only adding new items to (not removing from) allVals, the following solution would work. This simple snippet just shows you the basic idea, you'll need to adapt to your use case.
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([])
const addVal = () => {
setAllVals(allVals => {
// `i` would be the fixed index of newly added item
// it's captured in closure and would never change
const i = allVals.length
const setVal = (v) => setAllVals(allVals => {
const head = allVals.slice(0, i)
const target = allVals[i]
const tail = allVals.slice(i+1)
const nextTarget = { ...target, val: v }
return head.concat(nextTarget).concat(tail)
})
return allVals.concat({
val: '',
setVal,
})
})
}
return (
<input type="button" value="Add input" onClick={addVal} />
{allVals.map(v =>
<li><input value={v.val} onChange={(_e,newVal) => v.setVal(newVal)}></li>
)}
)
}
React hooks cannot be called in callbacks as this breaks the Rules of Hooks.
From what I've gathered you want to click the button and dynamically add inputs, and then be able to update each input. You can add a new element to the allVals array in the addVal callback, simply use a functional state update to append a new element to the end of the allVals array and return a new array reference. Similarly, in the updateVal callback use a functional state update to map the previous state array to a new array reference, using the index to match the element you want to update.
interface Foo {
val: string;
}
function MyReactFunction() {
const [allVals, setAllVals] = useState<Foo[]>([]);
const addVal = () => {
setAllVals((allVals) => allVals.concat({ val: "" }));
};
const updateVal = (index: number) => (e: any) => {
setAllVals((allVals) =>
allVals.map((el, i) =>
i === index
? {
...el,
val: e.target.value
}
: el
)
);
};
return (
<>
<input type="button" value="Add input" onClick={addVal} />
{allVals.map((v, i) => (
<li key={i}>
<input value={v.val} onChange={updateVal(i)} />
</li>
))}
</>
);
}

map not a function using react hooks

i'm trying to populate a select bar with a name from an API call. I Have created my hook, also useEffect for its side effects, and passed the data down the return. its giving me map is not a function error. my variable is an empty array but the setter of the variable is not assigning the value to my variable. How can i clear the map not a function error ? i have attached my snippet. Thanks.
import React, { useEffect, useState } from "react";
import axios from "axios";
const Sidebar = () => {
const [ingredients, setIngredients] = useState([]);
useEffect(() => {
const fetchIngredients = async (url) => {
try {
let res = await axios.get(url);
setIngredients(res.data);
} catch (error) {
setIngredients([]);
console.log(error);
}
};
fetchIngredients(
"https://www.thecocktaildb.com/api/json/v2/1/search.php?i=vodka"
);
}, []);
const displayIngredients = ingredients.map((ingredient) => {
setIngredients(ingredient.name);
return <option key={ingredient.name}>{ingredients}</option>;
});
return (
<div className="sidebar">
<label>
By ingredient:
<select>{displayIngredients}</select>
</label>
</div>
);
};
export default Sidebar
First, here
setIngredients(res.data);
change res.data to res.ingredients (the response object doesn't have data property). Then you'll face another bug,
const displayIngredients = ingredients.map((ingredient) => {
setIngredients(ingredient.name);
//...
First, ingredient.name is undefined, and second, it probably would be a string if it existed. Just ditch the setIngredients call here.
You are declaring displayIngredients as a variable typeof array (By directly affecting the array.map() result). You need it to be a function that return an array as follow :
const displayIngredients = () => ingredients.map((ingredient) => {
// Do not erase your previous values here
setIngredients(previousState => [...previousState, ingredient.name]);
// Changed it here as well, seems more logic to me
return <option key={ingredient.name}>{ingredient.name}</option>;
});
You should also wait for the API call to end before to display your select to prevent a blank result while your data load (If there is a lot). The easiest way to do that is returning a loader while the API call is running :
if(!ingredients.length) {
return <Loader />; // Or whatever you want
}
return (
<div className="sidebar">
<label>
By ingredient:
<select>{displayIngredients}</select>
</label>
</div>
);

React - How can I set "message from me"

I have 2 states:
const [formData, setFormData] = useState({ input: null });
const [outputData, setOutputData] = useState({ output: null });
formData is coming from my < input />. So, I am writing the input data by writing. outputData is coming from my GET api call.
I need to separate input data and output data. How can I tell the function if the message is from me?
function renderMessage(message) {
//const here
if (messageFromMe) {
return <Me />;
} else {
return <You />;
}
}
function Me(props) {
return (
<li className="Messages-message currentMember">
<div className="Message-content">
Hey its me
</div>
</li>
);
}
function You(props) {
return (
<li className="Messages-message">
<div className="Message-content">
Test
</div>
</li>
);
}
This might be a good application for useEffect hook. The useEffect hook takes 2 arguments, a callback function (this could be your renderMessage) and an array of values. Whenever a value in the array changes, useEffect will call your function passed into the first argument. The hook itself would be inside of a higher-level component that would effectively parent your You and Me components when they are rendered by the callback of the hook.
Now we can do this:
const [formData, setFormData] = useState(null);
const [outputData, setOutputData] = useState(null);
const [lastMessageSender, setLastMessageSender] = useState("");
let messageFromMe = (lastMessageSender === "me");
useEffect(()=>{renderMessage(message)}, [lastMessageSender]);
The last piece is this - whenever you call setFormData, also call setLastMessageSender and pass in the value "me" and whenever you call setOutputData, also call setLastMessageSender and pass in the value "you" - they could be other values eventually, such as the name of the user.
You may want to have convenience functions to help you code stay DRY...
const updateMessageFromOutput = payload => {
setOutputData(payload);
setLastMessageSender("you");
}
const updateMessageFromForm = payload => {
setFormData(payload);
setLastMessageSender("me");
}

Can someone explain how input functions are used in functions in reselect library?

https://github.com/reduxjs/reselect/blob/master/src/index.js#L89
export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
let lastArgs = null
let lastResult = null
// we reference arguments instead of spreading them for performance reasons
return function () {
if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
// apply arguments instead of spreading for performance.
lastResult = func.apply(null, arguments)
}
lastArgs = arguments
return lastResult
}
}
export function createSelectorCreator(memoize, ...memoizeOptions) {
return (...funcs) => {
let recomputations = 0
const resultFunc = funcs.pop()
const dependencies = getDependencies(funcs)
const memoizedResultFunc = memoize(
function () {
recomputations++
// apply arguments instead of spreading for performance.
return resultFunc.apply(null, arguments)
},
...memoizeOptions
)
...}
}
export const createSelector = createSelectorCreator(defaultMemoize)
So if I create createSelector(getUsers, (users) => users) for making a simple example. How is it run behind with the codes from above ?
createSelectorCreator(defaultMemoize) is called with getUsers, (users) => users inputs. Now defaultMemoize is also a function that returns a function. How are they all interacting to return the value ?
I think more important to how reselect works is why one should use it. The main reasons are composability and memomization:
Composability
Another way of saying this is that you write a selector once and re use it in other more detailed selectors. Lets say I have a state like this: {data:{people:[person,person ...]} Then I can write a filterPerson like this:
const selectData = state => state.data;
const selectDataEntity = createSelector(
selectData,//re use selectData
(_, entity) => entity,
(data, entity) => data[entity]
);
const filterDataEntity = createSelector(
selectDataEntity,//re use selectDataEntity
(a, b, filter) => filter,
(entities, filter) => entities.filter(filter)
);
If I move data to state.apiResult then I only need to change selectData, this maximizes re use of code and minimizes duplication of implementation.
Memoization
Memoization means that when you call a function with the same arguments multiple times the function will only be executed once. Pure functions return the same result given the same arguments no matter how many times they are called or when they are called.
This means that you don't need to execute the function when you call it again with the same parameters because you already know the result.
Memoization can be used to not call expensive functions (like filtering a large array). In React memoization is important because pure components will re render if props change:
const mapStateToProps = state => {
//already assuming where data is and people is not
// a constant
return {
USPeople: state.data.people.filter(person=>person.address.countey === US)
}
}
Even if state.data.people didn't change the filter function would return a new array every time.
How it works
Below is a re write of createSelector with some comments. Removed some code that would safety check parameters and allow you to call createSelector with an array of functions. Please comment if there is anything you have difficulty understanding.
const memoize = fn => {
let lastResult,
//initial last arguments is not going to be the same
// as anything you will pass to the function the first time
lastArguments = [{}];
return (...currentArgs) => {//returning memoized function
//check if currently passed arguments are the same as
// arguments passed last time
const sameArgs =
currentArgs.length === lastArguments.length &&
lastArguments.reduce(
(result, lastArg, index) =>
result && Object.is(lastArg, currentArgs[index]),
true
);
if (sameArgs) {
//current arguments are same as last so just
// return the last result and don't execute function
return lastResult;
}
//current arguments are not the same as last time
// or function called for the first time, execute the
// function and set last result
lastResult = fn.apply(null, currentArgs);
//set last args to current args
lastArguments = currentArgs;
//return result
return lastResult;
};
};
const createSelector = (...functions) => {
//get the last function by popping it off of functions
// this mutates functions so functions does not have the
// last function on it anymore
// also memoize the last function
const lastFunction = memoize(functions.pop());
//return a selector function
return (...args) => {
//execute all the functions (last was already removed)
const argsForLastFunction = functions.map(fn =>
fn.apply(null, args)
);
//return the result of a call to lastFunction with the
// result of the other functions as arguments
return lastFunction.apply(null, argsForLastFunction);
};
};
//selector to get data from state
const selectData = state => state.data;
//select a particular entity from state data
// has 2 arguments: state and entity where entity
// is a string (like 'people')
const selectDataEntity = createSelector(
selectData,
(_, entity) => entity,
(data, entity) => data[entity]
);
//select an entity from state data and filter it
// has 3 arguments: state, entity and filterFunction
// entity is string (like 'people') filter is a function like:
// person=>person.address.country === US
const filterDataEntity = createSelector(
selectDataEntity,
(a, b, filter) => filter,
(entities, filter) => entities.filter(filter)
);
//some constants
const US = 'US';
const PEOPLE = 'people';
//the state (like state from redux in connect or useSelector)
const state = {
data: {
people: [
{ address: { country: 'US' } },
{ address: { country: 'CA' } },
],
},
};
//the filter function to get people from the US
const filterPeopleUS = person =>
person.address.country === US;
//get people from the US first time
const peopleInUS1 = filterDataEntity(
state,
PEOPLE,
filterPeopleUS
);
//get people from the US second time
const peopleInUS2 = filterDataEntity(
state,
PEOPLE,
filterPeopleUS
);
console.log('people in the US:', peopleInUS1);
console.log(
'first and second time is same:',
peopleInUS1 === peopleInUS2
);

Resources