Using promises and exports in an atom #156
-
We have an atom that needs to make an AJAX request to get the data for the store. This isn't a problem as there are plenty of examples in the documentation about how to do this. However, we also need to have function exports that can update the store data. I didn't see any examples in the documentation for this scenario. First I tried using Here is what we're doing now. Is this a good approach? Do you see any problems or have any suggestions for improvements? export const uiAtom = atom('uiAtom', () => {
const store = injectStore<UiData>(
{
ready: false,
section: 'unknown',
sidebarOpen: true,
sidebarTheme: 'nileBlue',
theme: 'saphire',
},
{ subscribe: false },
);
const promise = injectMemo(() => {
const path = window.location.pathname;
return new Promise<void>((resolve) => {
// Default minimal state that the UI data will be merged into
let stateData: UiData = {
ready: false,
section: 'unknown',
sidebarOpen: true,
sidebarTheme: 'nileBlue',
theme: 'saphire',
};
if (['/login', '/password'].includes(path)) {
// No need to get navigation data for the login pages
stateData.ready = true;
stateData.section = 'login';
store.setState((state) => ({
...state,
...stateData,
}));
resolve();
}
// This is one of the main pages in the app or a site page
axios
.get(`${getMainApiUrl()}/ui?p=${window.location.pathname}`)
.then((res) => {
if (isStringWithValue(res.data.section)) {
stateData.ready = true;
stateData = {...stateDate, ...res.data}
store.setState((state) => ({
...state,
...stateData,
}));
} else {
toastError(
'Could not load initial UI configuration. Please refresh the page to try again. If the problem persists please contact support.',
);
}
resolve();
})
.catch((error) => {
handleRequestError(error);
resolve();
});
});
}, []);
// Set the default theme on initialization
injectEffect(() => {
setTheme('saphire', 'na');
}, []);
// Set up the atom exports.
const exports = {
/**
* Set the site name
*
* @param {string} siteName The site name
*/
setSiteName: (siteName: string) => {
store.setState((state) => ({
...state,
siteName,
}));
},
/**
* Set the theme value
* This will update the theme value in the UI and save it for the user
*
* @param {string} theme The theme value
* @param {string} source Where the updated data came from. 'user' or 'broadcast' are expected values
*/
setTheme: (theme: string, source: string = 'user') => {
// Don't do anything if the current value is the same as the new value.
// This can happen with the BroadcastChannel in page/Sidebar.tsx
// const oldState = promiseApi.store.getState();
const oldState = store.getState();
if (oldState.theme !== theme) {
// Update the uiAtom state
store.setState((state: UiData) => ({
...state,
theme,
}));
// Set the theme on the body tag
setTheme(theme, oldState?.theme);
if (source !== 'broadcast') {
// Post a message to the BroadcastChannel with the new theme value
themeChannel.postMessage(theme);
// Save the theme for the user
axios
.post(`${getApiUrl()}/ui/update-theme`, {
theme,
})
.catch((error) => {
handleRequestError(error);
});
}
}
},
/**
* Set the user data
* The data should include the user's name and email.
*
* @param {UiUser} user The user data - name and email
*/
setUser: (user: UiUser) => {
store.setState((state) => ({
...state,
user,
}));
},
};
return api(store).setExports(exports).setPromise(promise);
}); Thank you for any feedback you have. |
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 7 replies
-
Hey @erictompkins Your code looks fine to me. There's nothing wrong necessarily with kicking off promises in You're right, I don't see an example in the docs of using exports to manipulate the store returned by const initialState: UiData = {
section: 'unknown',
sidebarOpen: true,
sidebarTheme: 'nileBlue',
theme: 'saphire',
}
const uiAtom = atom('uiAtom', () => {
const promiseApi = injectPromise<UiData>(
async () => {
const path = window.location.pathname
// Default minimal state that the UI data will be merged into
let stateData = { ...initialState }
if (['/login', '/password'].includes(path)) {
// No need to get navigation data for the login pages
stateData.section = 'login'
return stateData // <- NOTE you didn't `return` here. Possible bug
}
// This is one of the main pages in the app or a site page
return axios
.get(`${getMainApiUrl()}/ui?p=${window.location.pathname}`)
.then(res => {
if (isStringWithValue(res.data.section)) {
stateData = { ...stateData, ...res.data } // <- NOTE you had `stateDate`. I'm guessing it's just not your exact code.
} else {
const errorMessage =
'Could not load initial UI configuration. Please refresh the page to try again. If the problem persists please contact support.'
toastError(errorMessage)
throw new Error(errorMessage)
}
return stateData
})
.catch(error => {
handleRequestError(error)
throw error
})
},
[],
{ initialState }
)
// Set the default theme on initialization
injectEffect(() => {
setTheme('saphire', 'na')
}, [])
// Set up the atom exports.
const exports = {
/**
* Set the site name
*
* @param {string} siteName The site name
*/
setSiteName: (siteName: string) =>
promiseApi.store.setStateDeep({ data: { siteName } }),
/**
* Set the theme value
* This will update the theme value in the UI and save it for the user
*
* @param {string} theme The theme value
* @param {string} source Where the updated data came from. 'user' or 'broadcast' are expected values
*/
setTheme: (theme: string, source = 'user') => {
// Don't do anything if the current value is the same as the new value.
// This can happen with the BroadcastChannel in page/Sidebar.tsx
const oldState = promiseApi.store.getState()
if (oldState.data?.theme !== theme) {
// Update the uiAtom state
promiseApi.store.setStateDeep({ data: { theme } })
// Set the theme on the body tag
setTheme(theme, oldState.data?.theme)
if (source !== 'broadcast') {
// Post a message to the BroadcastChannel with the new theme value
themeChannel.postMessage(theme)
// Save the theme for the user
axios
.post(`${getApiUrl()}/ui/update-theme`, {
theme,
})
.catch(error => {
handleRequestError(error)
})
}
}
},
/**
* Set the user data
* The data should include the user's name and email.
*
* @param {UiUser} user The user data - name and email
*/
setUser: (user: UiUser) =>
promiseApi.store.setStateDeep({ data: { user } }),
}
return promiseApi.setExports(exports)
}) I left two notes in there. I also got rid of the function Ui() {
const [state, { setTheme }] = useAtomState(uiAtom)
return (
<div>
Ui state: <pre>{JSON.stringify(state, null, 2)}</pre>
<button onClick={() => setTheme('new-theme')}>Set Theme</button>
</div>
)
}
function App() {
return (
<Suspense fallback={<div>Loading Ui...</div>}>
<Ui />
</Suspense>
)
} Can you see any points where your implementation with |
Beta Was this translation helpful? Give feedback.
-
I noticed that you used const [, { setTheme }] = useAtomState(uiAtom)
setTheme('value'); or const uiData = useAtomInstance(uiAtom);
uiData.exports.setTheme('value'); |
Beta Was this translation helpful? Give feedback.
Hey @erictompkins Your code looks fine to me. There's nothing wrong necessarily with kicking off promises in
injectMemo
. Assuming consumers wait for the set promise before usinguiAtom
, there wouldn't be a race condition between the promise and the exports.You're right, I don't see an example in the docs of using exports to manipulate the store returned by
injectPromise
. Here's how I would have written your code usinginjectPromise
: