Skip to content

Instantly share code, notes, and snippets.

@jonidelv
Last active June 28, 2023 21:55
Show Gist options
  • Save jonidelv/c813da85320a9b6490b502a20d9781b2 to your computer and use it in GitHub Desktop.
Save jonidelv/c813da85320a9b6490b502a20d9781b2 to your computer and use it in GitHub Desktop.
// Codesandbox of example working https://codesandbox.io/s/react-playground-forked-cmqx5d?file=/index.js:0-5046
import React, { useState, useCallback, useEffect, useMemo } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const httpPatch = (url, data) => {
return new Promise((resolve) => {
setTimeout(() => {
alert(`Patching ${url} with data: ${JSON.stringify(data)}`);
resolve({ success: true });
}, 1200);
});
};
const items = [
{
value: "1",
label: "First"
},
{
value: "2",
label: "Second"
},
{
value: "3",
label: "Third"
},
{
value: "4",
label: "Fourth"
},
{
value: "5",
label: "Fifth"
}
];
function onSelect(selectedItem) {
alert(selectedItem.label);
}
async function syncWithServer(selectedItem) {
try {
await httpPatch("user", { [`menu-state-${selectedItem.value}`]: true });
} catch (e) {
console.error(e);
}
}
function App() {
return (
<div>
<div className="content">
Normal example
<Dropdown label="select an option" items={items} onSelect={onSelect} />
</div>
<div className="content">
With a value selected
<Dropdown
label="select an option"
items={items}
onSelect={onSelect}
initialSelected={items[2]}
/>
</div>
<div className="content">
Searcheable example
<Dropdown
label="search an option"
items={items}
onSelect={onSelect}
searchable
/>
</div>
<div className="content">
Sync selection to the server
<Dropdown
label="search an option"
items={items}
onSelect={useCallback((selectedItem) => {
onSelect(selectedItem);
syncWithServer(selectedItem);
}, [])}
/>
</div>
</div>
);
}
function Dropdown({
label,
items = [],
initialSelected,
searchable,
onSelect
}) {
// If we have an initialSelected item, menu should be open
const [isOpen, setIsOpen] = useState(!!initialSelected);
const [selected, setSelected] = useState(initialSelected);
const [searchInput, setSearchInput] = useState("");
const onToggle = useCallback(() => {
setIsOpen((isOpen) => !isOpen);
}, []);
const onClose = useCallback(() => {
setIsOpen(false);
}, []);
const onItemClicked = useCallback(
(item) => {
console.log("item", item);
setSelected(item);
onSelect(item);
onClose();
},
[onSelect, onClose]
);
useEffect(() => {
if (!isOpen) {
setSelected(null);
setSearchInput("");
}
}, [isOpen]);
const options = useMemo(() => {
if (searchInput && items.length) {
return items.filter((item) =>
item.label.toLowerCase().includes(searchInput.toLowerCase())
);
}
return items;
}, [items, searchInput]);
return (
<div className="dropdown" tabIndex={0}>
<button
type="button"
className="dropdown-button"
id="dropdownButton"
aria-haspopup="true"
aria-expanded={isOpen}
onClick={onToggle}
>
{label}
</button>
<div
className={`${
isOpen ? "dropdown-open" : ""
} dropdown-menu dropdown-section`}
aria-labelledby="dropdownButton"
role="menu"
>
<>
{searchable && (
<input
className="search-input"
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
/>
)}
<ul>
{options.map((item) => (
<DropdownItem
key={item.value}
item={item}
selected={selected?.value === item?.value}
onClick={onItemClicked}
>
{item.label}
</DropdownItem>
))}
{searchable && !options.length && (
<li className="no-item">No items</li>
)}
</ul>
</>
</div>
</div>
);
}
function DropdownItem({ onClick, item, selected, children }) {
return (
<li className={selected ? "selected" : ""} onClick={() => onClick(item)}>
{children}
</li>
);
}
ReactDOM.render(<App />, document.getElementById("container"));
/*
Release note:
We’ve just released an update to our Dropdown component! It now includes a searchable option and the ability to sync selection with the server.
Props and API:
- `label`: A string that specifies the label for the dropdown button.
- `items`: An array of objects representing the options in the dropdown. Each object should have a `value` and a `label` property.
- `initialSelected`: An object representing the initially selected item in the dropdown. It should have a `value` and a `label` property.
- `searchable`: A boolean that specifies whether the dropdown should include a search input to filter the options.
- `onSelect`: A function that is called when an item is selected. It receives the selected item as an argument.
*/
window.currentUser = { id: '19', name: 'Jane', email: 'jane@chameleon.io' };
export const ActiveProfiles({ profiles, onLaunchProfile }) => {
var active = [];
for(i=0; i < profiles.length; i++) {
if(!profiles[i].disabled && profiles[i]['last_seen_time'] > new Date(new Date().getTime()-(24*60*1000)).toISOString()) { // within the last 24 hours
active.push(profiles[i]);
}
}
if(active.length == 1 && active[0].email === window.currentUser.email) {
active.length = 0;
}
return (
<div>
{active.map(function(a) { return <div onClick={() => onLaunchProfile(a.name, a.email)}>{a.name} - {a.email}</div> })}
</div>
)
}
@jonidelv
Copy link
Author

jonidelv commented Jun 28, 2023

@jonidelv
Copy link
Author

Release notes:

I’ve just released an update to our Dropdown component! It now includes a searchable option and the ability to sync selections with the server.

Props and API:

  • label: A string that specifies the label for the dropdown button.
  • items: An array of objects representing the options in the dropdown. Each object should have a value and a label property.
  • initialSelected: An object representing the initially selected item in the dropdown. It should have a value and a label property.
  • searchable: A boolean that specifies whether the dropdown should include a search input to filter the options.
  • onSelect: A function that is called when an item is selected. It receives the selected item as an argument.

@jonidelv
Copy link
Author

jonidelv commented Jun 28, 2023

Related to pr-1.js
Here are a couple of suggestions:

Instead of using a for loop to iterate over the profiles array, you could use the filter() method to create a new array with all elements that pass the test implemented by the provided function. This would make the code more concise and easier to read.

const active = profiles.filter(profile => !profile.disabled && profile.last_seen_time > new Date(new Date().getTime()-(24*60*1000)).toISOString());

You could also consider destructuring the profile object in the map function to make the code more readable.

{active.map(({ name, email }) => (
  <div onClick={() => onLaunchProfile(name, email)}>
    {name} - {email}
  </div>
))}

@jonidelv
Copy link
Author

You could use the useMemo hook to optimize the performance of the ActiveProfiles component by memoizing the active array. This would prevent unnecessary re-computations of the active array if the profiles prop hasn't changed.

Here's an example of how you could implement this:

const active = useMemo(() => {
    return profiles.filter(
      profile =>
        !profile.disabled &&
        profile.last_seen_time >
          new Date(new Date().getTime() - 24 * 60 * 1000).toISOString()
    );
  }, [profiles]);

This way, the active array will only be re-computed if the profiles prop changes.

@jonidelv
Copy link
Author

You could move the conditional statement that checks if the active array has only one element and if that element's email is equal to window.currentUser.email inside the useMemo hook. Instead of setting the length property of the active array to 0, you could return an empty array.

Here's an example of how you could implement this:

  const active = useMemo(() => {
    const filteredProfiles = profiles.filter(
      profile =>
        !profile.disabled &&
        profile.last_seen_time >
          new Date(new Date().getTime() - 24 * 60 * 1000).toISOString()
    );

    if (
      filteredProfiles.length === 1 &&
      filteredProfiles[0].email === window.currentUser.email
    ) {
      return [];
    }

    return filteredProfiles;
  }, [profiles]);

This way, the active array will be an empty array if the conditional statement is true.

@jonidelv
Copy link
Author

When rendering a list of elements in React, it's important to assign a unique key prop to each element. This helps React identify which items have changed, are added, or are removed, and can improve the performance of the component.

If the profiles array contains a unique identifier for each profile, such as an id, you could use that as the key prop.

return (
    <div>
      {active.map(({ id, name, email }) => (
        <div key={id} onClick={() => onLaunchProfile(name, email)}>
          {name} - {email}
        </div>
      ))}
    </div>
  );

@jonidelv
Copy link
Author

jonidelv commented Jun 28, 2023

This is how I would modify this component to utilize the Dropdown component created above.

import React, { useMemo, useCallback } from 'react';
import Dropdown from './Dropdown';

export const ActiveProfiles = ({ profiles, onLaunchProfile }) => {
  const active = useMemo(() => {
    const filteredProfiles = profiles.filter(
      profile =>
        !profile.disabled &&
        profile.last_seen_time >
          new Date(new Date().getTime() - 24 * 60 * 1000).toISOString()
    );

    if (
      filteredProfiles.length === 1 &&
      filteredProfiles[0].email === window.currentUser.email
    ) {
      return [];
    }

    return filteredProfiles;
  }, [profiles]);

  const items = active.map(({ id, name, email }) => ({
    value: id,
    label: `${name} - ${email}`,
    name,
    email
  }));

  const handleSelect = useCallback((selectedItem) => {
    onLaunchProfile(selectedItem.name, selectedItem.email);
  }, [onLaunchProfile]);

  return (
    <div>
      <Dropdown
        label="Select a profile"
        items={items}
        onSelect={handleSelect}
      />
    </div>
  );
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment