I'm working on a slightly complicated component that basically allows a user to type into an input, and then trigger a search (external API) for that product, the current issue however is that using the "Enter" key press, causes different behaviour and I want to sync up the behaviour of the "Find" button and "Enter". But before that I'm having some trouble on establishing where that check should happen, here's my React component:
export type CcceHook = {
allowForClassification: boolean,
classifyInProgress: boolean,
dataProfileId: string,
embedID: string,
handleCancelClassify: () => void,
handleClassify: (event?: SyntheticEvent<any>) => void,
handleCloseModal: () => void,
handleShowModal: () => void,
isDebugMode: boolean,
resultCode: string | null,
shouldShowModal: boolean,
};
// returns Ccce input fields based on the object form model - used in context provider
const getCcceValues = (object?: FormObjectModel | null) => {
const ccceInput: $Shape<CcceInput> = {};
//WHERE I THINK THE CHECK SHOULD GO (`ccceInput` is an object, with the `ccce.product` containing the users typed entry)
if (!object) {
return {};
}
// ccce input values
const ccceValues = object.attributeCollection.questions.reduce(
(acc, attribute) => {
const fieldEntry = ccceBeInformedFieldMap.get(attribute.key);
if (fieldEntry) {
acc[fieldEntry] = attribute.value;
}
return acc;
},
ccceInput
);
//check for null or empty string and if so hide "find goods button"
const productValueWithoutSpaces =
ccceValues.product && ccceValues.product.replace(/\s+/g, "");
const canClassify =
Object.values(ccceValues).every(Boolean) &&
Boolean(productValueWithoutSpaces);
return { canClassify, ccceValues };
};
export const useCcceEmbed = (
ccceResultAttribute: AttributeType,
onChange: Function
): CcceHook => {
const { object, form } = useFormObjectContext();
const [resultCode, setResultCode] = useState<string | null>(null);
const { canClassify, ccceValues } = getCcceValues(object);
const { handleSubmit } = useFormSubmit();
// data profile id is the 'api key' for 3ce
const dataProfileId = useSelector(
(state) => state.preferences[DATA_PROFILE_ID]
);
// data profile id is the 'api key' for 3ce
const isDebugMode = useSelector((state) => {
const value = state.preferences[CCCE_DEBUG_MODE_PREFERENCE];
try {
return JSON.parse(value);
} catch (error) {
throw new Error(
`3CE configuration error - non-boolean value for ${CCCE_DEBUG_MODE_PREFERENCE}: ${value}`
);
}
});
const [showModal, setShowModal] = useState<boolean>(false);
const handleCloseModal = useCallback(() => setShowModal(false), []);
const handleShowModal = useCallback(() => setShowModal(true), []);
// state value to keep track of a current active classification
const [classifyInProgress, setClassifyInProgress] = useState<boolean>(false);
// handle results from 3ce
const handleResult = useCallback(
(result) => {
if (result?.hsCode) {
onChange(ccceResultAttribute, result.hsCode);
setResultCode(result.hsCode);
setClassifyInProgress(false);
handleSubmit(form);
}
},
[ccceResultAttribute, form, handleSubmit, onChange]
);
const handleCancelClassify = useCallback(() => {
setClassifyInProgress(false);
handleCloseModal();
}, [handleCloseModal]);
// handle 3ce classify (https://github.com/3CETechnologies/embed)
const handleClassify = useCallback(
(event?: SyntheticEvent<any>) => {
if (event) {
event.preventDefault();
}
if (classifyInProgress || !canClassify) {
return;
}
const ccce = window.ccce;
if (!ccceValues || !ccce) {
throw new Error("Unable to classify - no values or not initialised");
}
setClassifyInProgress(true);
const classificationParameters = {
...ccceValues,
...DEFAULT_EMBED_PROPS,
};
ccce.classify(
classificationParameters,
handleResult,
handleCancelClassify
);
},
[
classifyInProgress,
canClassify,
ccceValues,
handleResult,
handleCancelClassify,
]
);
return {
allowForClassification: canClassify && !classifyInProgress,
classifyInProgress,
dataProfileId,
embedID: EMBED_ID,
handleCancelClassify,
handleClassify,
handleCloseModal,
handleShowModal,
isDebugMode,
resultCode,
shouldShowModal: showModal,
};
};
I have added a comment on where I think this logic should be handled (search "//WHERE I THINK..") - however, I'm unsure how to go from knowing the value of the users input, to checking for an enter press, I'm happy just to be able to console.log a user's key press, I should be able to tie up the logic from there, any advice would be really helpful.
TIA!
Related
I'm using useSWRInfinite() for data fetching in my React app, and on the first page I need to include the current timestamp as a cursor in the getKey function (as there's no previousPageData). I can't do it because on every millisecond the key is changing and invoking the fetcher function again. Any suggestion will sure be help!
Here is my code at the moment:
const useQuestions = () => {
const fetcher = async (url: string) => fetch(url).then((res) => res.json());
const timestamp = new Date().toISOString();
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.questions) return null;
const cursorQuery = `?cursor=${pageIndex ? previousPageData?.from : timestamp}`;
if (pageIndex && !previousPageData?.cursor) return null;
return `/api/questions${fromQuery}`;
};
const { data, size, setSize, error, isLoading, isValidating, mutate } = useSWRInfinite(getKey, fetcher, {
initialSize: 1,
revalidateAll: true,
revalidateFirstPage: false,
persistSize: true,
// I tried to set persistSize as true to prevent refetching when 1st page key is changing but no luck
});
The useMemeo hook might help you in this case. It should stop the key from changing at every millisecond.
const useQuestions = () => {
const fetcher = async (url: string) => fetch(url).then((res) => res.json());
const timestamp = useMemo(() => new Date().toISOString(), []);
const getKey = (pageIndex: number, previousPageData: any) => {
if (previousPageData && !previousPageData.questions) return null;
const cursorQuery = `?cursor=${pageIndex ? previousPageData?.from : timestamp}`;
if (pageIndex && !previousPageData?.cursor) return null;
return `/api/questions${fromQuery}`;
};
const { data, size, setSize, error, isLoading, isValidating, mutate } = useSWRInfinite(getKey, fetcher, {
initialSize: 1,
revalidateAll: true,
revalidateFirstPage: false,
persistSize: true,
});
};
The problem I'm having is, that I have a useContext in which I provide all logged users. On the initial run of the app or when the users' log in the array gets populated with all the users that are currently on the server... Which works as expected. But I have also the functionality, that whenever the server "user-connected" event runs, the front-end should just push the user to the end of this array. And there lays the problem. From the backend, the right user is sent, but when I access the connectedUsers array, the array is empty... but it should be already populated.
UsersProvider.tsx
export const inState = {
connectedUsers: [],
addUser: (user: any) => {},
sortUsers: (user: any, socketID: string) => {},
setLoggedUsers: () => {},
};
export interface initState {
connectedUsers: any[];
addUser(user: any): void;
sortUsers(users: any, socketID: string): void;
setLoggedUsers: React.Dispatch < React.SetStateAction < any[] >> ;
}
const UsersContext = createContext < initState > (inState);
export const useUsers = () => {
return useContext(UsersContext);
};
const initUserProps = (user: any) => {
user.messages = [];
user.hasNewMessages = false;
};
export const UsersProvider = ({
children
}: Props) => {
const [connectedUsers, setLoggedUsers] = useState < any[] > ([]);
const addUser = (user: any) => {
console.log('add', connectedUsers);
// This is empty, but it should be already populated when next user connected.
};
const sortUsers = (users: any, socketUserID: string) => {
const usersCopy = users;
usersCopy.forEach((u: any) => {
for (let i = 0; i < usersCopy.length; i++) {
const existingUser = usersCopy[i];
if (existingUser.userID === u.userID) {
existingUser.connected = u.connected;
break;
}
}
u.self = u.userID === socketUserID;
initUserProps(u);
});
// put the current user first, and sort by username
let sorted = usersCopy.sort((a: any, b: any) => {
if (a.self) return -1;
if (b.self) return 1;
if (a.username < b.username) return -1;
return a.username > b.username ? 1 : 0;
});
setLoggedUsers([...sorted]);
};
return ( <
UsersContext.Provider value = {
{
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
}
} >
{
children
} <
/UsersContext.Provider>
);
};
And the part of ChatBoard.tsx, you can find addUser function initiated whenever user-connected happens. I really don't know why the would array be empty, if it is populated on the first run with users event.
const ChatBoard = (props: Props) => {
const socket = useSocket();
const {
connectedUsers,
setLoggedUsers,
addUser,
sortUsers
} = useUsers();
useEffect(() => {
if (socket == null) return;
socket.on('users', (users) => {
console.log(users);
if (socket.userID) {
const socketID: string = socket ? .userID;
sortUsers(users, socketID);
}
});
socket.on('user-connected', (user: any) => {
console.log(user, 'this user connected!');
const connectingUser = user;
addUser(connectingUser);
});
socket.on('user-disconnected', (userID) => {
console.log('disconnected user');
const users = [...connectedUsers];
users.forEach((u) => {
if (u.userID === userID) {
u.connected = false;
setLoggedUsers([...users]);
}
});
});
return () => {
socket.off('users');
socket.off('user-connected');
};
}, [socket]);
CodeSandbox
So I have found the problem... so with React hooks sometimes a problem occurs called "Stale Closures", which means that React was picking up the old state (empty one, the one that was not yet populated and always returning that one.).
The solution to this problem, in my case is that when you use setState you use it with a callback. Like so, so you always get the latest state.
const addUser = (user: any) => {
setLoggedUsers((oldUsers) => {
const newUsers: any[] = [...oldUsers];
console.log(newUsers);
for (let i = 0; i < newUsers.length; i++) {
const existingUser = newUsers[i];
if (existingUser.userID === user.userID) {
existingUser.connected = true;
return newUsers;
}
}
initReactiveProperties(user);
newUsers.push(user);
return newUsers;
});
};
I am fetching data from my "backend" CMS - everything works fine, but when I want to setup default value I am getting error of undefined data.
My content is divided into some categories e.g.
const [category1, setCategory1] = useState([]);
const [category2, setCategory2] = useState([]);
Then I am fetching data from backend
useEffect(() => {
const fetchData = async () => {
const result = await client.query(
Prismic.Predicates.at('document.type', 'post'),
{ pageSize: 100 }
);
if (result) {
const category1Arr = [];
const category2Arr = [];
result.results.forEach((post) => {
switch (post.data.category[0].text) {
case 'Category1':
category1Arr.push(post);
break;
case 'Category2':
category2Arr.push(post);
break;
default:
console.warn('Missing blog post category.');
}
});
setCategory1(category1Arr);
setCategory2(category2Arr);
return setDocData(result);
} else {
console.warn(
'Not found'
);
}
};
fetchData();
}, []);
Code above works without any issues, BUT chosen category should have one post opened by default.
I am having menu when you can pick category and therefore I am using activeComponent function.
const [activeComponent, setActiveComponent] = useState('category1');
const modifyActiveComponent = React.useCallback(
(newActiveComponent) => {
setActiveComponent(newActiveComponent);
},
[setActiveComponent]
);
So category1 is active on default, therefore the category should also have default post.
This is what I tried:
const [postTitle, setPostTitle] = useState('');
const [postText, setPostText] = useState([]);
{activeComponent === 'category1' &&
category1.length > 0 && category1.map((post) => {
return ( <button onClick={()=> {setPostTitle(post.data.title[0].text); setPostText(post.data.body)}}
And data are shown typical just as a {postTitle} & {postText}
I tried to put default value in each category like this
useEffect(() => {
if (activeComponent === 'category1') {
setPostTitle(category1[2].data.title[0].text);
setPostText(category1[2].data.body);
}
if (activeComponent === 'category2') {
// same here just with category2 }
}, [activeComponent, category1, category2]);
But the code above gives me an error or undefined data even though it should be correct.
How can I achieve to make a default value with this logic above? Everything works like charm, just the default data does not work :(
This is array of objects:
In your last piece of code you have a typo, here:
useEffect(() => {
if (activeComponent === 'category1') {
setPostTitle(category1[2].data.title[0].text);
setPostText(category[2].data.body);
}
if (activeComponent === 'category2') {
// same here just with category2 }
}, [activeComponent, category1, category2]);
it should be:
useEffect(() => {
if (activeComponent === 'category1') {
setPostTitle(category1[2].data.title[0].text);
setPostText(category1[2].data.body);
}
if (activeComponent === 'category2') {
// same here just with category2 }
}, [activeComponent, category1, category2]);
in the first if statement, in second setPostText, you have category instead of category1.
I have this reducer function that I use for state management of my app.
const initialState = {roles: null};
const reducer = (draft, action) => {
switch (action.type) {
case 'initialize':
//what should i do here????
return;
case 'add':
draft.roles = {...draft.roles, action.role};
return;
case 'remove':
draft.roles = Object.filter(draft.roles, role => role.name != action.role.name);
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
to initialize my state I must use an async function that reads something from asyncStorage if it exists, must set draft.roles to it, if not it should be set to a default value.
const initialize = async () => {
try {
let temp = await cache.get();
if (temp == null) {
return defaultRoles;
} else {
return temp;
}
} catch (error) {
console.log('initialization Error: ', error);
return defaultRoles;
}
};
how can I get initilize function returned value inside 'initialize' case? if I use initilize().then(value=>draft.roles=value) I get this error:
TypeError: Proxy has already been revoked. No more operations are allowed to be performed on it
You cannot use asynchronous code inside of a reducer. You need to move that logic outside of the reducer itself. I am using a useEffect hook to trigger the initialize and then dispatching the results to the state.
There are quite a few syntax errors here -- should state.roles be an array or an object?
Here's my attempt to demonstrate how you can do this. Probably you want this as a Context Provider component rather than a hook but the logic is the same.
Javascript:
import { useEffect } from "react";
import { useImmerReducer } from "use-immer";
export const usePersistedReducer = () => {
const initialState = { roles: [], didInitialize: false };
const reducer = (draft, action) => {
switch (action.type) {
case "initialize":
// store all roles & flag as initialized
draft.roles = action.roles;
draft.didInitialize = true;
return;
case "add":
// add one role to the array
draft.roles.push(action.role);
return;
case "remove":
// remove role from the array based on name
draft.roles = draft.roles.filter(
(role) => role.name !== action.role.name
);
return;
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
useEffect(() => {
const defaultRoles = []; // ?? where does this come from?
// always returns an array of roles
const retrieveRoles = async () => {
try {
// does this need to be deserialized?
let temp = await cache.get();
// do you want to throw an error if null?
return temp === null ? defaultRoles : temp;
} catch (error) {
console.log("initialization Error: ", error);
return defaultRoles;
}
};
// define the function
const initialize = async() => {
// wait for the roles
const roles = await retrieveRoles();
// then dispatch
dispatch({type: 'initialize', roles});
}
// execute the function
initialize();
}, [dispatch]); // run once on mount - dispatch should not change
// should use another useEffect to push changes
useEffect(() => {
cache.set(state.roles);
}, [state.roles]); // run whenever roles changes
// maybe this should be a context provider instead of a hook
// but this is just an example
return [state, dispatch];
};
Typescript:
import { Draft } from "immer";
import { useEffect } from "react";
import { useImmerReducer } from "use-immer";
interface Role {
name: string;
}
interface State {
roles: Role[];
didInitialize: boolean;
}
type Action =
| {
type: "initialize";
roles: Role[];
}
| {
type: "add" | "remove";
role: Role;
};
// placeholder for the actual
declare const cache: { get(): Role[] | null; set(v: Role[]): void };
export const usePersistedReducer = () => {
const initialState: State = { roles: [], didInitialize: false };
const reducer = (draft: Draft<State>, action: Action) => {
switch (action.type) {
case "initialize":
// store all roles & flag as initialized
draft.roles = action.roles;
draft.didInitialize = true;
return;
case "add":
// add one role to the array
draft.roles.push(action.role);
return;
case "remove":
// remove role from the array based on name
draft.roles = draft.roles.filter(
(role) => role.name !== action.role.name
);
return;
}
};
const [state, dispatch] = useImmerReducer(reducer, initialState);
useEffect(() => {
const defaultRoles: Role[] = []; // ?? where does this come from?
// always returns an array of roles
const retrieveRoles = async () => {
try {
// does this need to be deserialized?
let temp = await cache.get();
// do you want to throw an error if null?
return temp === null ? defaultRoles : temp;
} catch (error) {
console.log("initialization Error: ", error);
return defaultRoles;
}
};
// define the function
const initialize = async() => {
// wait for the roles
const roles = await retrieveRoles();
// then dispatch
dispatch({type: 'initialize', roles});
}
// execute the function
initialize();
}, [dispatch]); // run once on mount - dispatch should not change
// should use another useEffect to push changes
useEffect(() => {
cache.set(state.roles);
}, [state.roles]); // run whenever roles changes
// maybe this should be a context provider instead of a hook
// but this is just an example
return [state, dispatch];
};
So I have a React Native application using TypeScript, with an error that's driving me crazy.
Basically, there is a Searchable List. It is initiated with an Array of values. Once the user types in a SearchBar, the initiated Array is filtered, returning an updated Array.
However, TypeScript gives me the error: Type '{ id: string; name: string; selected: boolean; }[]' provides no match for the signature '(prevState: undefined): undefined'.
I am confused because I don't know where this '(prevState: undefined): undefined'actually comes from and why. Do you know what I am doing wrong here? Help will be much appreciated.
Here is the code:
const defaultChoices = [
{
id: '1',
name: 'default',
selected: false,
},
];
let choicesList;
const getChoicesList = () => {
if (listName === 'include') {
choicesList = Object.values(includeChoices).map(choice => ({
...choice,
}));
} else if (listName === 'exclude') {
choicesList = Object.values(excludeChoices).map(choice => ({
...choice,
}));
}
};
const [filteredChoicesList, setFilteredChoicesList] = useState(choicesList);
useEffect(() => {
getChoicesList();
}, []);
useEffect(() => {
let choices = defaultChoices;
if (listName === 'include') {
choices = includeChoices;
} else if (listName === 'exclude') {
choices = excludeChoices;
} else {
choices = defaultChoices;
}
const newChoices = choices.filter(item => {
const itemData = `${item.name.toUpperCase()}`; // ignore Uppercase/Lowercase and make equal
const textData = query.toUpperCase();
return itemData.indexOf(textData) > -1;
});
setFilteredChoicesList(newChoices); // error occurs for "newChoices"
}, [effect]);
The problem is, you have set the choicesList as undefined. and getChoicesList is called on the first initialization of component but before that the state us being set where the choicesList is undefined.
Update the part of your code as
let choicesList: any[] = [];
const getChoicesList = () => {
let data: any[] = [];
if (listName === 'include') {
data = Object.values(includeChoices).map(choice => ({
...choice,
}));
} else if (listName === 'exclude') {
data = Object.values(excludeChoices).map(choice => ({
...choice,
}));
}
return data;
};
const [filteredChoicesList, setFilteredChoicesList] = useState<any[]>(choicesList);
useEffect(() => {
const updatedList = getChoicesList();
setFilteredChoicesList(updatedList)
}, []);
Now, you'll have the filteredChoicesList with data you need and you shouldn't have any compilation error.
Another thing is, create an interface and replace any with that interface in the state and while defining.