How to pass data from ActionFunction to Loader function in Remix.run? - javascript

I'm making an application with Remix.run. It consists of multiple steps where user fills forms and in the end gets success screen. All form steps are located on single route, but success is on it's own route.
How everything works
I have redux-like (actually XState, but it is not important) message processing on server. It receives current (or initial for initial load) state, message type and data from fields. It then returns new state to client and page is rendered based on that state. Server don't have any storage an I don't want to introduce one, since it will complicate things.
// form page
export async function loader() {
return json(await getInitialState())
}
export async function action({request}) {
let fd = await request.formData();
let {current_state, message_type, ...data} = Object.fromEntries(fd.entries());
return json(await getNextState(current_state, {type: message_type, data}));
}
export function unstable_shouldReload({submission}) {
return !submission
}
export default FormPage() {
let loaderData = useLoaderData();
let actionData = useActionData();
// awful, isn't it?
let currentState = actionData || loaderData;
}
My problems
First my problem was that loader was triggered on every form submit. So, ActionFunction returns new state, but LoaderFunction returns initial state. It would be fine, but to get initial state, loader runs API, so it's wasted calls. I solved it with unstable_ShouldReload, and it's kind of ok, but not really.
Now I got to final step of the form and feel that I have same problem, but now I have separate route, so I can't just prevent running LoaderFunction. I need LoaderFunction to actually run on new page, and get data passed from previous steps for it. Basically, it's the same problem - I need to use data posted in form usable inside loader.
I tried to replace code above using session cookie with all the data, but I have other problems with it. Action returns cookie with session data, loader reads it and returns new state. Unfortunately it fails in few scenarios. If there is current session, user can reload page, prevent sending info again and get result as data was sent. It is not how web "usually" works. I can change it, by destroying session on each load, but then very first POST request yields no result, if there is no JS on user side. Otherwise it is best solution yet (but limited by size of the cookie).
Question
How to pass data from last action to next loader, or process POST request in loader itself?

How to pass data from last action to next loader
The only way is using sessions and or cookies. If you want to only be able to read the session data once and ensure it's not there anymore after a reload you could use session.flash(key, value), then in your loader, when you do session.get(key) it will remove the flash key from the session, if you commit the session in the loader then the cookie will be updated to don't have that key anymore, after a reload it will not be there.
process POST request in loader itself
This is not possible, loader functions only handle GET and OPTIONS requests, for every other request method only the action function is used.

Related

How to have a request scoped variable in remix JS?

I'm new to remix Js and wanted to understand what the best way is to get this done.
I have an app that calls a function which in turn calls an API to load user details. This user information is used to paint multiple things like header, footer and load the appropriate view for the user.
The thing I notice is that currently for a single page load request from the browser, the function (and in turn the API) is called independently multiple times by the module that renders the header, footer, page etc. What I wanted to know is if it is possible to have the user details saved in a variable so that for a single request all the modules can use this variable and it needs to be only fetched once per browser request.
I tried the following but it did not work
let userDetails;
export function getUserDetails(userId){
if(!userDetails){
console.log("Calling API to fetch user details);
//call the API
userDetails = async getUserById(userId);
}
return userDetails;
}
One option we have is to save the user details in the browser/session but that is not allowed due to some policies. What would be the best way to achieve this so that we only need to call the API once per request from the browser and save subsequent calls for a single page load to fetch the same info ?
TLDR : what would be the best way to initialize an object in an remix server module so that its value could be accessible by other modules and it has to be set only once per a request from a browser.
Dataloader can be used to solve this issue.
It offers request-level caching (and batching) which means parallel loaders on the initial HTML request can cooperate while remaining independent.
Example usage with remix can be found here.
// Root.tsx
export const loader = async ({ context }) => {
const user = await context.loaders.userById.get("user_1");
return json({ user });
}
// ...
// Child.tsx
export const loader = async ({ context }) => {
// This will await the same promise created in the Root.tsx loader
const user = await context.loaders.userById.get("user_1");
return json({ user });
}
// ...
Having said that, I would warn that the abstraction cost of dataloader is quite high and to only use it if you really can't afford to call into your API multiple times (loaders run in parallel so it's more an issue of upstream load over performance).
It's also worth noting that SPA navigations won't see the benefit because each loader is called as a separate request from the browser so a request-level cache will be ineffective.

Inertia Manual Share State Lazily Inside Controller not Working

I'm going crazy because of this one, I have this address form in a very nested component and a route to validate the form input data. In the controller, I want to return back to the same page but share the form data to be able to fetch it in a parent component. I'm trying to follow Inertia DOCs to lazily share the data to be available to all components but for some reason this isn't working!
1- I'm submitting the form:
const submitAddressCheck = () => {
shippingDetailsForm.post(
route("cart.checkaddress", [props.webshop_slug]),
{}
);
};
2- The form gets validated as expected but it doesn't share the data globally to all components.
CartController.php
public function checkaddress(StoreAddressCheckRequest $request)
{
Inertia::share(
'testing',
fn ($request) => $request
? $request
: null
);
return redirect()->back();
}
Once I submit the form it gets validated and that's it, no new props passed, the data isn't being shared to my parent component or anything. Am I missing something?
Inertia::share() will not persist any data passed between requests. So if you share data and then redirect to another route, then your shared data will not be passed.
You are probably looking for Flash messages. With flash messages you use the with() method when redirecting to show errors, success messages, anything you like. Make sure you follow the documentation and add the code to the HandleInertiaRequests middleware.
When it is time to redirect, you do something like this:
return redirect()->back()->with('testing', $request->all());

How to Pass Data from Server using SSR in SvelteKit?

I'm used to using Express with a templating engine, like Handlebars. I want to start working with Svelte and SvelteKit, but I'm unsure how to start working with both. I am stuck on passing data to the frontend from the server. In Express, I'd normally connect to the database, and then pass the data to res.render, where the templating engine would then take over. So far, I think I have to run a handle function to pass the data, and then I can access it from my page. But it seems that the handle function runs for every request, and if all my pages require different data, does that mean I have to use a giant switch statement or something for every page?
Can anybody tell me if that's the right way to pass data over, or if there's a better way. Sorry, I'm fairly new to metaf rameworks and Svelte.
There are two ways to achieve what you want to do.
Let for both cases assume you have an about page and want to show dynamic data on this page like some team members. You would have a file called about.svelte (this makes the /about route) with in it something like:
<script>
export let team = [];
</script>
{#each team as member}
...
{/each}
Now how to get the team data to the page itself ?
the load function
The first option is the load function, this is a function that runs before the page is loaded and can be used to fetch data for this page. You would put the following block in about.svelte, usually before the other script block:
<script context="module">
export async function load({ fetch }) {
const team = await fetch('/api/team').then(res => res.json());
return {
props: {
team
}
}
}
</script>
Important to note with this one is that you need some sort of api (/api/team in this case) that can give you the data.
a page endpoint
The second option is to make a so called page endpoint this acts as a kind of api and lives next to the page itself. In the same folder as about.svelte add a file about.js:
export async function get() {
const team = []; // add team data here somehow
return {
status: 200,
body: {
team
}
}
what's the difference ?
When to use which approach is mostly up to you, but you have to remember the following two things:
The load function will likely need another api somewhere (this does not have to be SvelteKit)
The load function is, on the first page, executed on the server and afterwards will always run on the client. The endpoint on the other hand always runs serverside.

Collection on client not changing on Meteor.subscribe with React

I am using Meteor with React JS.
I get the collection of "list" by this code,
Meteor.subscribe('getList', {status:'active'},function(){
self.setState({lists:Lists.find().fetch()});
});
Here is the code for publish,
Meteor.publish('getList', function(data){
data.user = this.userId;
return Lists.find(data);
});
So it is working. The problem is that I have two components that calling Meteor.subscribe('getList'). But the status is not the same.
So in other component, I have this code,
Meteor.subscribe('getList', {status:'archived'},function(){
self.setState({lists:Lists.find().fetch()});
});
So what happens here is, if the user go to FirstComponent, this.state.lists is empty (which is correct). Then when the user navigate to SecondComponent, this.state.lists is populated with data (which is correct). But when the user go back to FirstComponent, this.state.lists still populated with data (which is wrong).
It is like that the first collection (is empty) in client is still there. Then the second collection (not empty) is added. I want to clear the collection in client before subscribing again.
By the way I am using flow-router.
Since subscriptions are cumulative you should repeat the query condition in your setState functions:
let query = {status:'active'};
Meteor.subscribe('getList',query,function(){
self.setState({lists:Lists.find(query).fetch()});
});
query = {status:'archived'};
Meteor.subscribe('getList',query,function(){
self.setState({lists:Lists.find(query).fetch()});
});
Please note however that from a security perspective you really don't want to pass a query object from the (untrusted) client as a parameter to the subscription! From the console someone can just do:
Meteor.subscribe('getList',{});
It looks like you are setting the React state in the Meteor.subscribe onReady callback. Does each component have it's own state?
Where are you calling Meteor.subscribe from? Remember that Meteor puts subscription data in local mini-mongo and you are reading from there when you call Lists.find().
The way you have this set up, the data will not be reactive. You need to store handle to the Meteor.subscribe cursor and manage the subscription lifecycle with that.
If you could share more code, I could give a more concise answer.

AngularJS ui-router save previous state

I want to save the current state, I don't mean saving url and params, I want to save the entire view with scopes.. In fact, I want to implement a facebook like search system, but differently. I want to display the results on the entire page, so I'll replace the current main State, and when the search bar will be cleared, then we will restore the previous state. I don't want to rebuild the state to restore the state instantly (no server requests) + to avoid complicated operations (eg. infinite scroll data..). I've found ui'router extras (sticky state) plugin, but we need to define what state to save with states definitions..
I apologize for my bad english..
You can save the date in the local storage using angular local storage. It will save you a trip to the server for data.
This is an architectural decision. The answer to this question will not be short and can be very subjective and different people will have different ways to implementing it.
What I would suggest is that you leverage local storage and do all actions on data present in local storage.
The way you would do this is to first understand that all server requests need to be done through A service that return what's present in localstorage is network is offline.
So all AJAX calls will be done from the controller like this
var getMeRequest = AuthService.getMe();
getMeRequest.promise.then(function(response){
if(response.cached) {
// $update views if need be
} else {
//Some code
}
});
And in the AuthService file, you'd do it like this:
// In AuthService
this.getMe() = function () {
if(network.online) {
//Make AJAX call
// Update localstorage with whatever response
return {response: locastorageData, cached:false};
} else {
return {response: localstorageData, cached:true};
}
};
This will not only let you make all the ajax calls as if you were online, but also allow the controller to update the view based on whether the response is cached or not.

Categories