Should I filter within my flux stores? - flux

This is essentially a matter of best practices for sync vs. async operations in the store/view.
I'm attempting to build a real-time search field for a list of items.
In my current implementation, I request all the items from my server and save them to a store. When a user enters characters into the search field, the view filters the items using the .filter() native function. Unfortunately, this causes some delay (due to the number of items and the complexity of the filter) before the next rendering (which includes the keypressed character displaying in the search field).
My question is: should I instead call an action to initialize the filtering of the items in the store, and update whenever the store is complete? Then, in the meantime, I would be able to render the keypressed character before the filtered results come in.
Is there an intuitive way to prevent/abort a previous, incomplete request to filter when a new one comes in?
Edit:
Here's the new implementation:
The component/view
_onChange() {
this.setState({
items: ItemStore.getFilteredItems()
})
},
handleSearchChange(event) {
this.setState({
searchText: event.target.value,
})
ItemActions.filterItems(event.target.value)
},
render() {...}
The action
filterItems(searchTerm) {
dispatcher.dispatch({
type: FILTER_ITEMS,
searchTerm: searchTerm,
});
}
The store
var _store = {
items: [],
filteredItems: []
}
var filter = function (searchTerm) {...}
...
Dispatcher.register(function (action) {
switch (action.type) {
case FILTER_ITEMS:
filter(action.searchTerm)
ItemStore.emit(CHANGE_EVENT)
break
}
})
Edit 2:
I've ended up adding a setTimeout when dispatching within the action to make it async. I also split the list of items and the search into two different components so that the time required to re-render the list does not affect/block the search field component.

The filtering should not be blocking the keypressed character in the search field. Basically, because the filtering is a heavy operation, treat it as if it were an async HTTP event.
Here's how you should be doing in a Flux world:
Retrieve all items from the server and save them to the store
Each time you type in the search field, set the state for the input field and the component should re-render immediately (https://facebook.github.io/react/docs/forms.html#controlled-components)
At the same time as you set the state of the text, also dispatch an action to filter the results. The filtered results should be put in the store as a separate entity and updated as a result of this action.
When the store updates the filtered results, your component should have them as a prop and re-render automatically (independent of the keypress events)

Related

adding logic to redux reducer to persist state

I am working with a react/redux learning project, where I'm building components to host headless CMS content. One part of the application is a dropdown that will select the content from all available content channels in the source CMS.
This works on the first pass, but when I navigate to another page (ie, the detail of a single CMS content item - the first page displays multiple items in a grid) it resets the state back to an initial (empty) variable.
The component is below:
import { FETCH_CHANNELS } from '../actions/types';
// set the initial state first
const initialState = {
isLoading: true,
data: [],
error: ""
}
// set the state depending on the dispatch coming in
const channelsReducer = (state = initialState, action) => {
switch(action.type) {
case FETCH_CHANNELS:
// reduce the returned state down to an array of options
// filter here to limit to searchable only
const activeChannels = [];
action.payload['channels'].forEach( el => {
if(el.isChannelSearchable) {
let singleItem = {
key: el.channelId,
value: el.channelId,
text: el.channelName,
type: el.channelType
}
activeChannels.push(singleItem);
}
});
return {...state, data: activeChannels, isLoading: false};
case "ERROR":
return {...state, error: action.msg};
default:
return state;
}
}
export default channelsReducer;
My issue here (as I see it), is the initialisation of the initialState constant at the beginning, everytime that the component is refreshed, it is set to empty again. Makes sense.
How can I persist the state that is returned in the FETCH_CHANNELS case (that action is a call to a back end api that returns all channels) so that upon the component remounting it still retains it's state?
Not sure if I have to either (quite possibly none of these are correct):
Attempt with some logic in the front end component that is calling this action, to not call it if data already exists?
Create another piece of state in the redux store and update that state from the front end component once a value from the drop down has been selected?
or 3. Try and handle it here with setting a variable in the reducer and logic to return that if necessary?
Like I said, I'm building this to try and learn a bit about react and redux, but i'm really not sure what the way to handle this is...
update, as suspected... neither of those options were correct. I was not calling the link correctly in the component generating the click event to drill into the detail content item. Implementing Link from react-router-dom was the right way to handle this.

How do you get data back from a react-redux store?

Using React-Redux
I have a select list that when the user chooses one of the options, a item is created and placed in the database (if it matters, the reason its a select box is that there are multiple variations of the same item and what variation is important).
This is working correctly.
My problem is that I am not sure how I can get the id of the new item out of the redux store.
And just for chuckles, the prior developer set all this up with sagas. So I am still coming up to speed on how it all works together.
So when the select box is checked, the function checkFunction is called that calls the function createItem in the saga file. These functions are below:
in Repositories.jsx
checkFunction = (data) => {
const {createItem} = this.props;
// data holds the info that we need to send to the action
const created = createItem(data);
// once data comes back, redirect the user ot the details of the created item
// need id of created item
// navigateTo(`/item-details/${created.id}`);
}
in Repositories.saga.js
export function* createItem(action) {
try {
const {payload: newItem} = action;
// call api to create item
const created = yield call(requestAPI.post, ITEMS_URL, newItem);
// send message that its been done
yield put(actions.repositories.createItem.ok(created));
// send message to refresh item list
yield put(actions.inventories.fetchItems.start());
} catch (e) {
yield put(actions.repositories.createItem.fail(e));
}
}
I don't understand how to return the id of the created item once its created. I feel like I am missing something basic here. Can anyone point me in the right direction?
Actually getting data from saga back to react component is not trivial. There are multiple approaches to do what you need although each has its downside.
1. Call navigateTo in the saga.
export function* createItem(action) {
...
const created = yield call(requestAPI.post, ITEMS_URL, newItem);
yield call(navigateTo, `/item-details/${created.id}`)
}
This would be my recommended solution if you can get the navigateTo function into the saga. Navigation is a side effect and sagas are there to deal with side effects. Make sure to use the call effect, changing the url by directly calling the function can lead to some issues.
2. Store the latest created item id in redux store
In your reducer, when action actions.repositories.createItem.ok(created) is dispatched, store the created item info and then send the latest created item to the component. Finally you can use componentDidUpdate or useEffect to call navigateTo when the prop changes.
render() {
const created = Redux.useSelector(state => state.created);
useEffect(() => navigateTo(`/item-details/${created.id}`), [created])
...
}
This has the disadvantage that the component will rerender becuase of the changed created value.
3. Send callback in the createItem action
You can put a function into your action and then call it from the saga and essentially using it as a callback.
Component:
checkFunction = (data) => {
const {createItem} = this.props;
// data holds the info that we need to send to the action
const created = createItem(data, (created) => {
navigateTo(`/item-details/${created.id}`);
});
}
Saga:
export function* createItem(action) {
...
const created = yield call(requestAPI.post, ITEMS_URL, newItem);
action.callback(created)
...
}
The problem with this approach is that functions are not serializable and so you ideally should avoid them in your actions. Also, technically, there could be multiple sagas handling the same action and then it gets kind of confusing who should call the callback.

Deleting Apollo Client cache for a given query and every set of variables

I have a filtered list of items based on a getAllItems query, which takes a filter and an order by option as arguments.
After creating a new item, I want to delete the cache for this query, no matter what variables were passed. I don't know how to do this.
I don't think updating the cache is an option. Methods mentionned in Apollo Client documentation (Updating the cache after a mutation, refetchQueries and update) all seem to need a given set of variables, but since the filter is a complex object (with some text information), I would need to update the cache for every given set of variables that were previously submitted. I don't know how to do this. Plus, only the server does know how this new item impact pagination and ordering.
I don't think fetch-policy (for instance setting it to cache-and-network) is what I'm looking for, because if accessing the network is what I want after having created a new item, when I'm just filtering the list (typing in a string to search), I want to stay with the default behavior (cache-only).
client.resetStore would reset the store for all type of queries (not only the getAllItems query), so I don't think it's what I'm looking for either.
I'm pretty sure I'm missing something here.
There's no officially supported way of doing this in the current version of Apollo but there is a workaround.
In your update function, after creating an item, you can iterate through the cache and delete all nodes where the key starts with the typename you are trying to remove from the cache. e.g.
// Loop through all the data in our cache
// And delete any items where the key start with "Item"
// This empties the cache of all of our items and
// forces a refetch of the data only when it is next requested.
Object.keys(cache.data.data).forEach(key =>
key.match(/^Item/) && cache.data.delete(key)
)
This works for queries that exist a number of times in the cache with different variables, i.e. paginated queries.
I wrote an article on Medium that goes in to much more detail on how this works as well as an implementation example and alternative solution that is more complicated but works better in a small number of use cases. Since this article goes in to more detail on a concept I have already explained in this answer, I believe it is ok to share here: https://medium.com/#martinseanhunt/how-to-invalidate-cached-data-in-apollo-and-handle-updating-paginated-queries-379e4b9e4698
this worked for me (requires apollo 2 for cache eviction feature) - clears query matched by regexp from cache
after clearing cache query will be automatically refeteched without need to trigger refetch manually (if you are using angular: gql.watch().valueChanges will perform xhr request and emit new value)
export const deleteQueryFromCache = (cache: any, matcher: string | RegExp): void => {
const rootQuery = cache.data.data.ROOT_QUERY;
Object.keys(rootQuery).forEach(key => {
if (key.match(matcher)) {
cache.evict({ id: "ROOT_QUERY", fieldName: key })
}
});
}
ngrx like
resolvers = {
removeTask(
parent,
{ id },
{ cache, getCacheKey }: { cache: InMemoryCache | any; getCacheKey: any }
) {
const key = getCacheKey({ __typename: "Task", id });
const { [key]: deleted, ...data } = cache.data.data;
cache.data.data = { ...data };
return id;
}
}

BehaviorSubject 'grouped'

I'm just getting started with RXJS to see if it can replace my currently manual data streams. One thing I'm trying to port is a situation whereby the last value in the stream is remembered, so future observers will always get the 'current' value as well as subsequent ones. This seems to be fulfilled by BehaviorSubject.
However, I need to do this for a group of entities. For example, I might have data that represents a message from a user:
{ userId: 1, message: "Hello!" }
And I want a BehaviorSubject-like object that'll store the last message for all users. Is this something I can do with RXJS out-of-the-box, or would I need to do it myself? (If so, any pointers would be appreciated).
EDIT: After some more thought, it perhaps seems logical to having an 'incoming' subject, an observer that updates a Map, and then a function which I can call which initialises an Observable from the map values, and merges with the incoming stream...?
I use RxJS with a redux-like state setup. I have a BehaviorSubject that holds the current state, and every time an event/action is fired that current state gets passed through functions that produce a new state, which the subject is subscribed to.
Here's a simplified version of what I use:
export default class Flux {
constructor(state) {
//all resources are saved here for disposal (for example, when hot loading)
//this is just a flux dispatcher
this.dispatcher = new rx.Subject(),
// BehaviorSuject constructed with initial state
this.state = new Rx.BehaviorSubject(state),
}
addStore(store, initialState, feature = store.feature) {
this.dispatcher
.share()
// this is where the "reduction" happens. store is a reducer that
// takes an existing state and returns the new state
.flatMap(({action, payload}) =>
store(this.state.getValue(), action, payload))
.startWith(initialState || {})
.subscribe(this.state)
);
return this;
}
addActions(actions: rx.Subject) {
// actions are fed to the dispatcher
this.resources.push(actions.share().subscribe(this.dispatcher));
return this;
}
}
I create a global Flux object with manages state. Then, for every "feature" or "page" or whatever I wish I add actions and stores. It makes managing state very easy, and things like time-travel are something that are a given with Rx.

Calling multiple views in CouchApp query

I need to search the CouchDB based on several criteria entered in a form. Name, an array of Tags and so on. I would then need various views to index on these fields. Ultimately, all the results will be collated in data.js and provided to mustache.html. Say there are 3 views - docsByName, docsByTags, docsById.
What I don't know is, how to query all these views in query.js. Can this be done and how ?
Or should the approach be of that to write one view that makes multiple emits for each search somehow ?
Thank you.
From what you say I assume you are using Evently, so I will quote from Evently primer:
The async function is the main star, which in this case makes an Ajax request (but it can do anything it wants). Another important thing to note is that the first argument to the async function is a callback which you use to tell Evently when you are done with your asynchronous action. [...] Whatever you pass to the callback function then becomes the first item passed to the data function.
In short: put your Ajax requests in async.js.
As a side note: Evently is only one of the possible choices to write a couchapp and it is not clear if it is maintained. However it works and it is easy to rearrange the code to not use it.
EDIT: here is a sample async function (cut&paste from an old program):
function(cb, e) {
var app = $$(this).app
;
app.db.openDoc('SOMEDOCID', {
error: function(code, error, reason) {
alert("Error("+code+" "+error+"): "+reason);
}
, success: function(doc) {
app.view('SOMEVIEWNAME', {
include_docs: true
, error: function(code, error, reason) {
alert("Error("+code+" "+error+"): "+reason);
}
, success: function(resp) {
resp.doc = doc;
cb(resp);
}
});
}
});
}

Resources